10 Commits
Author SHA1 Message Date
admin 357cb5b4ac Merge pull request 'docs: add StarForth primitive word reference' (#2) from claude/wizardly-tesla-36lqlt into master
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Reviewed-on: https://gitea.strshipos.org/admin/LithosAnanake/pulls/2
2026-09-29 05:22:50 +00:00
Claude 2fcc468ecb docs: add StarForth primitive word reference
Lists every C-registered StarForth primitive with stack notation and
usage notes, grouped by module, plus a section on implementation quirks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BY9HMwK5Cetz3caBgHGyds
2026-09-29 05:19:09 +00:00
Robert Allan JamesandClaude Sonnet 5 6302dcb50e FABRIC-3.7.md: record Phase 8 v3 closure (in-system block-copy defense)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Distinguishes it clearly from the still-accepted "cloned outside
StarshipOS entirely" limitation this document already settled -- Phase
8 v3 closes a narrower, different threat: cloning block content using
StarshipOS's own console primitives, now refused by MOVE/CMOVE/CMOVE>/
RELOCATE-BLOCK for cross-device copies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:45:06 -04:00
Robert Allan JamesandClaude Sonnet 5 26c1117ccd Phase 8 v3: refuse a same-VM, cross-device raw block copy from within StarshipOS
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Two research passes confirmed the identity record itself (seed/pubkey/
cert) is already unreachable from any FORTH primitive -- only C-level
read_devblock/write_devblock touch it. But a device's ordinary user block
content CAN be copied between two attached devices today, using only
stock, unpinned words: <src> BLOCK <dst> BUFFER 1024 MOVE UPDATE
SAVE-BUFFERS, or the dedicated RELOCATE-BLOCK word (whose own doc comment
already admits "performs no policy validation of its own"). Checked
whether the existing per-block owner_fp/BLK-ACL-ALLOW@ metadata already
solves this -- it doesn't: owner_fp encodes who (a VM identity pubkey),
never where (physical device), and blk_get_buffer()/blk_update() never
consult acl_allow/acl_ttl at all -- those fields are completely inert.

Small, targeted fix, no rearchitecture:

- blk_subsys_relocate_block() (block_subsystem.c): same-device check.
  Its own documented purpose is wear-leveling (relocate on the SAME
  device) -- never stated as cross-device, and nothing enforced that
  until now.
- New public blk_lbn_device_handle() (block_subsystem.c/.h): the missing
  LBN-to-device direction (blk_get_device_range() already goes the other
  way). Opaque, stable, == comparable.
- MOVE (memory_words.c) and CMOVE/CMOVE> (string_words.c): refuse when
  both addresses are block-window addresses backed by two different
  devices -- the exact shape of the composed attack. A copy where either
  end is ordinary VM memory (the overwhelming common case: staging text
  from PAD, editing a block in place) is untouched.
- blk_vm_check_epoch()/blk_vm_slot_for_addr() exposed (block_words.h) so
  the two new call sites share the same window-slot invalidation contract
  rather than a second, divergent copy of it.

Verified live on all three architectures, not just boot-clean: same-
device MOVE/RELOCATE-BLOCK still succeed exactly as before; cross-device
MOVE/CMOVE/RELOCATE-BLOCK all refused. Zero UNKNOWN WORD, identical
dict_hash across all three (this change adds no FORTH-visible word, only
internal refusal conditions, as predicted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:44:22 -04:00
Robert Allan JamesandClaude Sonnet 5 b7d9ed5425 FABRIC-3.7.md: settle drive-cloning defense as rejected, not undesigned
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Captain Bob rejected a PIN/passphrase second factor firmly and directly
after it was built and live-tested on all three architectures: "nothing
like a pin or a password or secret code or any bullshit... Everybody has
secrets. There's only the drive." All PIN-related code (KDF, XOR
keystream seed encryption, no-echo input, MINT/WIREBIND prompts,
user_identity_seed_t v3 format) was reverted before commit -- none of it
ever landed in git history.

This project's identity model has no knowledge factor, by design:
physical possession of the thumbdrive is the entire credential. A
byte-for-byte clone being equivalent to the real drive is the accepted
model, not a gap needing a fix. Recorded here so future work doesn't
default back to a PIN/password approach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 03:42:38 -04:00
Robert Allan JamesandClaude Sonnet 5 81049da268 Correct FABRIC-3.7.md: the original elevation-entrypoint bug diagnosis was wrong
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Found while starting Part A's implementation, before any code was written
against the original design: reading the actual deleted SEND-ELEVATE-REQUEST
source (git show 3e201c8^:capsules/common/messaging.4th, block 5040) shows
it copied the target word's name as literal character bytes into a scratch
buffer, building "S" <name-text>" <pk0> <pk1> <pk2> <pk3> ELEVATE-GRANT"
entirely in the sending VM's own memory, then sent that finished string --
never a raw address -- to Hera. waddr/wu never crossed the VM boundary as
numbers anywhere in this flow. The original write-up reasoned from
ELEVATE-GRANT's own signature alone, without first reading how the caller
actually built its message.

Corrected in place, wrong original text kept struck-through for
traceability rather than deleted, per this series' own convention.

Net effect: Part A (the buffer/message redesign) is not needed -- the
original mechanism was already safe. Part B (capsules/zuse.4th, already
committed and three-arch verified this session, 089ab21) stands on its
own, unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 23:03:47 -04:00
Robert Allan JamesandClaude Sonnet 5 089ab2160e Phase 8 Part B: gate ZUSE-ELIGIBILITY-ADD, closing the no-drive-needed privilege path
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
ZUSE-ELIGIBILITY-ADD's own doc comment admitted "no authorization check
here or anywhere else... applied later if and when actually needed --
not invented here." That's now: anyone reaching a Hera FORTH prompt
could add their own pubkey to the eligibility list with zero legitimate
identity material -- no minted drive, no WIREBIND, no cert-signature
check involved at all. Once a future caller reaches ELEVATE-GRANT again,
a self-added pubkey would pass zuse_eligibility_is_member() and grant
ACL-ALLOW!/ACL-TTL! on any named word.

Fixed the FORTH-only way, matching this project's own convention (ACL
policy belongs in ACL.4th, never in C; never gate on zuse_session in C --
her power is the absence of ACLs, not a hardcoded session check):
ZUSE-ELIGIBILITY-ADD is now denied by default (capsules/zuse.4th block
4016), granted and pinned only inside ACL-ZUSE-BOOT's already-existing
authenticated branch (block 4017) -- the same gate her own god-mode
already goes through, requiring a real cert-verified Zuse before it opens.

Live-verified on all three architectures, not just boot-clean: after
genesis authentication, ACL-ALLOW@ and ACL-PINNED? both read -1, and
HERE ZUSE-ELIGIBILITY-ADD executes successfully past the ACL gate.

Phase 8 v1 plan: /home/rajames/.claude/plans/jiggly-cuddling-stallman.md
Part A (the ELEVATE-GRANT pointer-confusion fix, FABRIC-3.7.md) is
separate, not yet built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 22:57:10 -04:00
Robert Allan JamesandClaude Sonnet 5 2406158668 Fix include/version.h collision between hosted Makefile and Makefile.starkernel
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
The hosted Makefile writes include/version.h with FORCE as a prerequisite
(always regenerates), but Makefile.starkernel's own rule had no
prerequisite at all -- Make only rebuilds a target with no prerequisites
when the file is missing. Since both Makefiles write the same path with
incompatible content (the hosted version has no LITHOS_VERSION/
LITHOS_VERSION_STR at all), running a bare `make -f Makefile.starkernel`
after a hosted `make` build silently reused the wrong file and failed
deep in kernel_main.c with "LITHOS_VERSION_STR undeclared".

Added FORCE (declared .PHONY, matching the hosted Makefile's own existing
pattern) as include/version.h's prerequisite in Makefile.starkernel.
Verified the fix directly: poisoned version.h with a hosted build, then
ran a bare (non-clean) kernel build and confirmed it self-heals.

Full three-architecture acceptance: amd64 (logs/20260922-215333/),
aarch64 (logs/20260922-215803/), riscv64 (logs/20260922-220217/) -- all
three reach [zuse@Hera] ok>, zero UNKNOWN WORD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 22:34:54 -04:00
Robert Allan JamesandClaude Sonnet 5 2008c4596a Add FABRIC-3.7.md: Phase 8 PKI elevation-entrypoint design, fixes a real pointer-confusion hole
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
New document, not a reopening of the closed FABRIC-3.5.md/FABRIC-3.6.md --
successor for exactly one topic, per those documents' own close discipline.

Records a security defect found by inspection while auditing Phase 4's
collateral damage to ELEVATE-GRANT: the old SEND-ELEVATE-REQUEST mechanism
passed a raw address (waddr/wu) computed in the sending VM's own memory
space across to Hera, which dereferences it in Hera's own space --
per-VM vaddr_t means those are never the same address space. Whoever
controls waddr controls what dictionary entry NAME>XT resolves to on
Hera, independent of the caller's actual pubkey/eligibility.

Design fix: never cross an address, only ever cross bytes -- generalizes
this session's own payload-aliasing fix (SkHermesMessage.payload_buf) one
level up. Send the target word's name as inline payload bytes, copy them
into a fixed kernel-owned buffer already in Hera's own memory on receipt,
and hand vm_interpret() only kernel-controlled integer literals referencing
that buffer. ELEVATE-GRANT itself is unchanged -- policy logic stays in
FORTH, per ACL.4th's own rule.

No code written or authorized by this document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:44:06 -04:00
Robert Allan JamesandClaude Sonnet 5 7306872848 Phase 5.7: archival close of FABRIC-3.5.md and FABRIC-3.6.md at v2.1.0
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
FABRIC-3.5.md's provisional "design phase closed, not yet archival" header
replaced with the real CLOSED/ARCHIVAL form per its own §XXVI.5 spec,
naming v2.1.0 -- prior status headers kept underneath, not deleted.

FABRIC-3.6.md gets the same treatment: a CLOSED/ARCHIVAL banner above the
START HERE section, which stays as historical record rather than being
removed. Neither closure triggers the FABRIC-0 -> -1 -> -2 -> -3
carry-forward chain (both are standalone topic documents) and neither
touches FABRIC-3.md, which remains open for its own topic.

The Tripod/kernel reshuffle is complete: Hermes moved into the kernel as
kernel-Hermes, the Tripod is Hera/Artemis/Hestia, tagged v2.1.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:53:57 -04:00
30 changed files with 83562 additions and 37 deletions
+20 -31
View File
@@ -1,37 +1,26 @@
# FABRIC-3.5.md — the Tripod/kernel reshuffle
**Status: REOPENED 2026-09-19, by direct instruction ("reopen 3.5 and write it up as a gap
analysis section"), to add §XXXI — a gap-analysis sweep of the whole FABRIC set. The design
phase remains closed; §XXXI adds findings, not new design.**
**Status: CLOSED/ARCHIVAL as of 2026-09-22, at tag `v2.1.0`.** Per §XXVI.5's own close
condition ("this document closes when the tag exists"). Produced the full design ruling for
the Tripod/kernel reshuffle: Hermes moves from a FORTH VM into the kernel as kernel-Hermes; the
Tripod becomes Hera/Artemis/Hestia; every design question this document opened is ruled (§XXX.7
/ §XLI's punch list). Execution against that design is recorded in `FABRIC-3.6.md`, not here —
all five phases (0–5) closed there, ending in the `v2.1.0` tag this header names. **Nothing is
carried forward and no successor is created** — this document is not part of the
`FABRIC-0 → -1 → -2 → -3` chain (§XXVI.5), so closing it hands nothing to a successor.
**`FABRIC-3.md` remains open, living and authoritative for its own topic** (bare metal boot);
nothing here ever superseded it.
> **Prior status, kept rather than overwritten: DESIGN PHASE CLOSED as of 2026-09-19, by
> direct instruction ("close the document for now"). Not yet archival.** That close stood for
> the duration of the sweep and its substance is unchanged — every design question was and
> remains ruled. The reopen is recorded rather than the close deleted, per this series' own
> rule against silently rewriting a prior state.
**What is closed, and what is deliberately not.** Every design question this document opened is
ruled — see §XXX.7. **No code has been written and no code is authorized.** The execution
sequence (§XXVI.6, as amended by §XXX.7) is untouched: the surgical strip, the build, the
Isabelle/HOL pass, the documentation sweep, the SBOM, the merge to `master`, and the `v2.1.0`
tag all remain ahead.
**This is therefore a narrower close than §XXVI.5 specified**, and the difference is stated
rather than glossed. §XXVI.5 ruled that this document closes *when the tag exists*, in
`FABRIC-2.md`'s CLOSED/ARCHIVAL form, naming the tag it closed at. **The tag does not exist**,
so claiming that close would be a label running ahead of the real state — the exact error
`FABRIC-3.md` §I.2 corrected when it rolled `LITHOS_VERSION` back, and the same standard §XXIX
applied to the LTS question. **The archival close specified by §XXVI.5 still stands and still
happens at `v2.1.0`.** This header is the provisional one the instruction's own "for now" asks
for.
**Nothing is carried forward and no successor is created.** As §XXVI.5 establishes, this
document is not part of the `FABRIC-0 → -1 → -2 → -3` chain, so closing it hands nothing to a
successor. **`FABRIC-3.md` remains open, living and authoritative for its own topic** (bare
metal boot); nothing here supersedes it. The live artifact from this document is its punch list
(§XXVI.6 / §XXX.7) — that, not this header, is what the build works from.
**Open by design, not by omission:** items 10–17 of §XXVI.6 are execution, not decisions.
> **Prior status headers, kept rather than overwritten, per this series' own rule against
> silently rewriting a prior state:**
>
> **REOPENED 2026-09-19**, by direct instruction ("reopen 3.5 and write it up as a gap analysis
> section"), to add §XXXI — a gap-analysis sweep of the whole FABRIC set.
>
> **DESIGN PHASE CLOSED as of 2026-09-19**, by direct instruction ("close the document for
> now"). Not yet archival at that point — the execution sequence (surgical strip, build,
> Isabelle pass, doc sweep, SBOM, merge, tag) was still entirely ahead. That gap is now closed:
> `FABRIC-3.6.md`'s Phases 0–5 are the execution this header once described as pending.
Three things recorded here are explicitly *outside* this reshuffle and still need their own
authorization — the `.claude/CLAUDE.md`/`MANIFEST.md` documentation reconciliation (§XXVI.1),
the `src/*.c.bak` hygiene question (§XXII.5), and the stray `refs/heads/v2.0.1` branch and
+41 -4
View File
@@ -1,6 +1,24 @@
# FABRIC-3.6.md — the Tripod/kernel reshuffle: execution log
> ## START HERE — session handoff
**Status: CLOSED/ARCHIVAL as of 2026-09-22, at tag `v2.1.0`.** All five phases (0–5) closed —
Category A strip, Hestia relocation/birth, kernel-Hermes build (allocator, arena, message
types), Stage-cutover of every FORTH-owned message type, the Category B strip (Hermes VM +
`messaging.4th` removed), the Isabelle pass, the documentation sweep, `make sbom`, the version
bump, and the merge to `master` + tag. **This document is closed; the reshuffle it tracked is
done, not merely planned.** Per §XXVI.5 (cited here, ruled in `FABRIC-3.5.md`, also closed at
this same tag): closing this document hands nothing to a successor and does not touch
`FABRIC-3.md`, which remains open and authoritative for its own topic (bare metal boot).
**The `START HERE` section immediately below is kept as historical record of how this
execution began — it describes a session about to start work, not the current state.** Do not
follow its "first action: task 0.0" instruction; every task it points at is closed. If future
work touches this fleet again (a Phase 8 PKI elevation entrypoint, further hardware bring-up,
etc.), it gets its own new document, not a reopening of this one — matching the discipline
`FABRIC-3.5.md`'s own close just followed.
---
> ## START HERE — session handoff (historical — reshuffle complete, see status above)
>
> **If you have just been told "go build it", read this section, then `FABRIC-3.5.md`'s §XLI,
> then start at task 0.0 below. Do not re-derive the design — it is settled.**
@@ -1345,7 +1363,7 @@ intermediate states.
build its own entrypoint rather than resuming `SEND-ELEVATE-REQUEST`. No further action
this phase.
## Phase 5 — Close-out
## Phase 5 — Close-out — **COMPLETE, tag `v2.1.0` cut 2026-09-22**
**Note on commit granularity:** tasks 5.1–5.4 landed as one commit (`a4ad14a`), a deliberate
deviation from the per-task-commit discipline this document has otherwise used since Phase 0.
@@ -1511,8 +1529,27 @@ opposite of what per-task commits are for. Tasks 5.5–5.7 return to one-commit-
closed `2026-09-18T21:28:26Z` — 20 seconds later, before any of this reshuffle's actual
work existed on the branch. Already closed, already not merged, blocks nothing. Nothing
to resolve beyond recording what it is.
- [ ] **5.6** — Merge to `master`; tag `v2.1.0`.
- [ ] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
- [x] **5.6** — Merge to `master`; tag `v2.1.0`.
2026-09-22 · **Closed, explicit approval obtained first (this is a shared-branch
operation on the sole production line — the plan authorizing it is not a standing
grant, per `.claude/CLAUDE.md`'s own scope rule).** `master` was a strict ancestor of
this branch (`git merge-base --is-ancestor origin/master HEAD` — true, 0 commits unique
to `master`, 85 unique to this branch), so this was a clean fast-forward, not a merge
commit: `git push origin HEAD:refs/heads/master` (`e56974e..8edbd3c`), never checking out
`master` locally, so the uncommitted `FABRIC-3.md` WIP on this branch's working tree was
never touched. Tagged `v2.1.0` (annotated, `8edbd3c`) and pushed. `refs/heads/v2.0.1`
(task 5.5's finding — fully merged, safe to delete) also deleted this same pass, explicit
approval obtained first.
- [x] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
2026-09-22 · **Closed.** `FABRIC-3.5.md`'s provisional "design phase closed, not yet
archival" header replaced with the real CLOSED/ARCHIVAL form, naming `v2.1.0`, per its
own §XXVI.5 spec — prior status headers kept underneath, not deleted, matching this
series' own no-silent-rewrite rule. This document (`FABRIC-3.6.md`) closed the same way:
a CLOSED/ARCHIVAL status banner added above the `START HERE` section, which is kept as
historical record of how the session began rather than removed. Neither closure triggers
the `FABRIC-0 → -1 → -2 → -3` carry-forward chain (both documents are standalone topic
documents, not part of it) and neither touches `FABRIC-3.md`, which stays open for its
own topic. **Phase 5, and the Tripod/kernel reshuffle itself, are complete.**
---
+205
View File
@@ -0,0 +1,205 @@
# FABRIC-3.7.md — Phase 8 PKI: the elevation entrypoint
**Status: OPEN — design only, no code written or authorized.**
**CORRECTION (2026-09-22, before any code was written against this document): §2's central
claim — that the old `SEND-ELEVATE-REQUEST` passed a raw cross-VM address into `ELEVATE-GRANT`'s
`waddr` — is wrong.** Found while starting Part A's implementation: reading the actual deleted
source (`git show 3e201c8^:capsules/common/messaging.4th`, block 5040) shows
`SEND-ELEVATE-REQUEST` copied the target word's **name as literal character bytes** (via
`ELEVATE-REQ-APPEND`'s `CMOVE`) into a scratch buffer, building the text `S" <name-text>" <pk0>
<pk1> <pk2> <pk3> ELEVATE-GRANT`, and sent *that whole string* to Hera. When Hera's own
interpreter runs `S" <name-text>"`, it allocates a fresh string **in Hera's own memory** and
pushes Hera's own valid address — no numeric cross-VM address ever appears anywhere in this
flow. §2 was written from `ELEVATE-GRANT`'s signature alone, assuming the caller forwarded a raw
address, without first reading how the caller actually built its message. It didn't.
**What is still real, much narrower than originally claimed:** if a future caller ever spliced
*attacker-influenced* text into the name field without checking for an embedded `"` character,
that could break out of the `S" ... "` literal early and inject arbitrary FORTH source, executed
with Hera's privilege. That's an input-validation discipline question for whoever writes the new
caller (validate: no embedded `"`, or just always use a compile-time-fixed literal name, never a
runtime-supplied one) — not an architectural cross-VM-memory defect requiring the buffer/message
redesign §3 originally called for. **§3's proposed mechanism (Hera-side fixed receive buffer,
kernel-constructed integer-literal-only command) is not needed** — the original text-copy
design was already safe against the bug as actually diagnosed. Kept below, struck through, for
traceability, per this series' own rule against silently rewriting a prior state.
Successor to
`FABRIC-3.5.md`/`FABRIC-3.6.md` (both CLOSED/ARCHIVAL at `v2.1.0`) for exactly one topic: the
Ed25519-challenge-response elevation entrypoint that `.claude/CLAUDE.md`'s ACL section names as
Phase 8, the last open item in the word-level ACL system. This is a **new document**, not a
reopening of `FABRIC-3.6.md` — that document's own close header says future fleet work gets its
own document, and this is that.
**Provenance.** Written 2026-09-22, immediately after `FABRIC-3.6.md`'s close, from a design
conversation with Captain Bob about a security concern he raised directly: how to rebuild the
elevation entrypoint that Phase 4's Category B strip left dangling, without reopening a hole.
The design below was proposed, and Captain Bob asked for it in writing here rather than left
only in session memory.
---
## 1. What's dangling, and why
`FABRIC-3.6.md` Phase 4 (Category B strip, 2026-09-22) deleted `capsules/common/messaging.4th`
after every FORTH-owned message type had been cut over to kernel-Hermes. One casualty was
collateral, not intended: `SEND-ELEVATE-REQUEST` (`messaging.4th` block 5040) was the only
caller of both `KH-ELEVATE-SEND` (`src/starkernel/repl.c`) and, transitively, `ELEVATE-GRANT`
(`capsules/zuse-eligibility.4th`, blocks 4021–4022). All three still exist in the tree.
`ELEVATE-GRANT` is still loaded at boot (`capsules/init.4th:18`). Nothing can call it any more.
Captain Bob's decision at the time (`FABRIC-3.6.md`'s own Phase 4 entry): leave it unreachable,
don't patch a caller back in as part of that strip. Phase 8 builds its own entrypoint instead of
resuming this one. **This document is that entrypoint's design.**
<details>
<summary>Original §2/§3 (WRONG — see the correction at the top of this document; kept for
traceability, not current design)</summary>
### 2. The security hole in the old mechanism — found before any code was written
`ELEVATE-GRANT`'s signature, unchanged since it was written:
```
ELEVATE-GRANT ( waddr wu pk0 pk1 pk2 pk3 -- )
```
`waddr`/`wu` are an address/length pair meant to point at the string naming the word to elevate.
`pk0`–`pk3` are the caller's Ed25519 pubkey, packed 8 bytes per cell (`ELEVATE-PUBKEY-UNPACK`,
`mama_forth_words.c`).
**The old `SEND-ELEVATE-REQUEST` computed `waddr` in the *sending* VM's own address space, but
`ELEVATE-GRANT` always executes on Hera** (`ELEVATE-GRANT always runs on Hera` — `repl.c`'s own
comment on `KH-ELEVATE-SEND`, still there). A word's name string lives in the sending VM's
memory. `ELEVATE-GRANT` dereferences `waddr` in Hera's memory. Those are not the same address
space by construction — `vaddr_t` is per-VM.
**Consequence:** whoever controls `waddr` controls what bytes `NAME>XT` reads and resolves as a
word name, in Hera's dictionary, not the caller's. This is not "the string might be malformed" —
it is a primitive for making Hera's own `ELEVATE-GRANT` grant `ACL-ALLOW!`/`ACL-TTL!` on
*whatever dictionary entry the attacker's chosen `waddr` happens to land on*, regardless of what
word name the caller claims to be requesting elevation for. A caller who can influence `waddr`
at all — not forge a signature, not defeat `zuse_eligibility_is_member()`, just choose a number
— has a privilege-escalation primitive against the fleet governor.
This was never exploited (the entrypoint has had zero live callers since the file that called it
was deleted), and is reported here as a design defect found by inspection, not a live incident.
### 3. The fix: never cross an address, only ever cross bytes
This project already solved the general version of this problem once, this same session
(`FABRIC-3.6.md` tasks 3.8/3.9, the payload-aliasing fix): a kernel-Hermes message's payload
must be **copied into the message's own storage**, never a pointer into the sender's memory that
might be reused or freed before the receiver drains it. `SkHermesMessage.payload_buf`
(`include/starkernel/vm/kernel_hermes.h:160`, `SK_HERMES_CHUNK_MAX_PAYLOAD` = 1024 bytes) is
exactly that fix, already built, already proven on all three architectures.
**The elevation entrypoint's hole is the same defect one level up: an address crossing a
boundary it isn't valid on the other side of.** The fix generalizes directly:
1. **Never send `waddr`/`wu` across the kernel-Hermes boundary.** Send the pubkey (32 bytes,
already the right shape for `payload_buf`) and the target word's **name, as literal bytes**,
copied inline into the message payload — not an address, the actual characters. This is
already how `CONSOLE-CMD-EVENT`'s payload works (a command string's bytes, not a pointer to
one), so this isn't a new pattern, it's applying the existing one to the one caller that
still passed a raw address.
2. **On receipt, kernel-Hermes's C drain-checkpoint copies those name bytes into a small,
fixed, kernel-owned buffer that already lives in Hera's own VM memory** — a receive-side
mirror of the existing send-side pattern (`g_kh_elevate_buf`, `repl.c:445`, is the
already-built precedent for "a static buffer this mechanism owns"; this needs its Hera-side
counterpart). The buffer's address is a compile-time constant, known to the kernel, never
computed from anything the caller supplied.
3. **The FORTH command handed to `vm_interpret()` on Hera references only that fixed buffer's
address and length as plain integer literals.** Both are always kernel-controlled. Neither is
ever derived from caller input. `ELEVATE-GRANT` itself does not change — same signature, same
`zuse_eligibility_is_member()` check, same `ACL-ALLOW!`/`ACL-TTL!` grant. Policy logic stays
in FORTH, per `ACL.4th`'s own rule (no new C primitive for policy) — this fix is entirely
about how bytes get from one VM to another, not about who is allowed to grant what.
**Why this closes the hole structurally, not by validation:** there is no string to sanitize and
no address to bounds-check, because the interpreted command never contains anything an attacker
touched except opaque data bytes that get copied, never dereferenced as a pointer, by the
receiving side. The class of bug (cross-address-space pointer confusion) becomes impossible by
construction, the same way `payload_buf` made use-after-free impossible by construction rather
than by careful lifetime tracking.
</details>
## 2 (corrected). What the old mechanism actually did, and the one real gap in it
Re-read from the actual deleted source (`git show 3e201c8^:capsules/common/messaging.4th`,
blocks 5039–5040): `SEND-ELEVATE-REQUEST ( pk3 pk2 pk1 pk0 waddr wu -- )` used `waddr`/`wu` only
to `CMOVE` the target word's **name bytes**, as text, into a scratch buffer
(`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`) it owned — building the literal string `S"
<name-text>" <pk0> <pk1> <pk2> <pk3> ELEVATE-GRANT` entirely in the *sending* VM's own memory.
Only that finished string — not `waddr` itself — went to `KH-ELEVATE-SEND` and across to Hera.
When Hera's interpreter runs `S" <name-text>"`, Hera's own `S"` allocates a fresh string **in
Hera's own memory** and pushes Hera's own valid address. `waddr`/`wu` never cross the VM
boundary as numbers at any point — only as copied character content. There is no cross-VM
pointer dereference anywhere in this flow.
**The one real, much narrower gap:** the name text is spliced into `S" ... "` with no check for
an embedded `"` character. If a future caller ever passed attacker-influenced text as the name
(none ever did — the word had zero live callers), a `"` in the name would close the string
literal early and let the rest of the name execute as raw FORTH source, with Hera's privilege.
This is a caller-discipline / input-validation question, not an architectural defect: either
always use a compile-time-fixed name literal at the call site (no runtime input, no risk at
all), or validate for an embedded `"` before building the command if a name ever does need to
come from something less trusted than the call site's own source code.
**Net effect on Phase 8 v1's scope:** Part A, as originally conceived in §3 above, is not
needed. If a `SEND-ELEVATE-REQUEST` replacement is ever built, it can follow the original
text-copy design as-is, with the one-line `"`-check added if and only if the name is ever
runtime-supplied rather than a fixed literal. No kernel-Hermes/`repl.c` changes required. Part B
(`capsules/zuse.4th`, gating `ZUSE-ELIGIBILITY-ADD`) stands on its own, independently verified,
unaffected by this correction.
## 4. What Phase 8 actually needs to build
Corrected per §2's re-read above. Concretely, when Phase 8 next picks this up:
- If a caller into `ELEVATE-GRANT` is ever needed again, rebuild it close to the original
`SEND-ELEVATE-REQUEST` shape (`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`/text-copy into `S" ...
"`) — it was already safe. Add the one-line embedded-`"` check only if the name is ever
runtime-supplied rather than a call-site literal. No `kernel_hermes.c`/`kernel_hermes.h`/
`repl.c` changes needed for this.
- `ELEVATE-GRANT` unchanged either way.
- **Part B is done** (`capsules/zuse.4th`, committed and three-arch verified this session,
2026-09-22) — `ZUSE-ELIGIBILITY-ADD` denied by default, granted only inside `ACL-ZUSE-BOOT`'s
authenticated branch. This closes the actual "grant yourself eligibility with no real drive at
all" path — a real, independently-confirmed gap, unaffected by this correction.
- **Defending against a cloned drive — settled, 2026-09-23: not going to happen, by design.**
Today, WIREBIND/MINT trust whatever identity is stored on an attached thumbdrive with no
challenge at all (confirmed by grep: no `ed25519_sign`/`ed25519_verify` call anywhere in
`capsule_wirebind.c` or `capsule_mint.c`), and the private key seed itself is stored in
plaintext on the drive, read in the same devblock as the pubkey/cert. **This is accepted, not
a gap.** Captain Bob, directly: *"nothing like a pin or a password or secret code or any
bullshit... Everybody has secrets. There's only the drive."* Physical possession of the drive
is the entire, deliberate credential model — a byte-for-byte clone being equivalent to the
real drive is the accepted design, not a defect to close. A PIN/passphrase second factor was
built, live-tested on all three architectures, and fully reverted before commit
(`/home/rajames/.claude/plans/jiggly-cuddling-stallman.md`, now marked rejected; memory
`feedback_no_knowledge_factor_identity`) — **do not revisit a knowledge-factor approach here.**
Any future work in this space needs a fundamentally different mechanism (not something typed
and known) or stays an accepted limitation.
- **Phase 8 v3, 2026-09-23 — done, a distinct and narrower concern from the item above.** The
"accepted limitation" above is about a drive image copied *outside* StarshipOS entirely (e.g.
imaged on an external computer) — that's still accepted, unchanged by this item. Captain Bob
separately asked to close a narrower, different threat: **cloning a device's block content
from *within* StarshipOS's own console**, using its own stock, unpinned words
(`<src> BLOCK <dst> BUFFER 1024 MOVE`/`RELOCATE-BLOCK`). That's now closed — `MOVE`/`CMOVE`/
`CMOVE>`/`RELOCATE-BLOCK` all refuse a same-VM, cross-device copy, verified live on all three
architectures. See `/home/rajames/.claude/plans/jiggly-cuddling-stallman.md`'s "Phase 8 v3"
section for the full design and verification record. The identity record itself
(seed/pubkey/cert) was already unreachable from FORTH before this — this closes the one real
gap the research found: ordinary block content, not the identity record.
## 5. What this document is not
Not a reopening of `FABRIC-3.6.md`, not a change to anything currently built, not an
authorization to write code. Per this series' own convention: design here, execution gets its
own document when the work actually starts, the same relationship `FABRIC-3.5.md` had to
`FABRIC-3.6.md`.
+10 -1
View File
@@ -597,6 +597,7 @@ KERNEL_OBJS := \
.PHONY: qemu qemu-esp qemu-gdb
.PHONY: thumbdrive iso-usb
.PHONY: info help
.PHONY: FORCE
# ==============================================================================
# MAIN TARGETS
@@ -627,7 +628,15 @@ clean-kernel:
# BUILD RULES
# ==============================================================================
include/version.h:
FORCE:
# FORCE prerequisite (matches the hosted Makefile's own pattern, Makefile:613/626):
# this file is also written by the hosted Makefile with different, incompatible
# content (no LITHOS_VERSION/LITHOS_VERSION_STR) -- without FORCE, a bare build
# here after a hosted `make` run silently reuses that stale/wrong-flavored file
# and fails with "LITHOS_VERSION_STR undeclared" deep in kernel_main.c, instead
# of regenerating its own correct version.
include/version.h: FORCE
@mkdir -p include
@BUILD_TS=$$(date -Iseconds 2>/dev/null || echo "unknown"); \
printf '#ifndef STARFORTH_VERSION_H\n#define STARFORTH_VERSION_H\n\n' > $@; \
+4
View File
@@ -12,6 +12,8 @@ Block 4016
( >BODY-then-store, so a pinned CONSTANT isn't tamper-proof. )
( Read with ZUSE-PUBKEY@ / ZUSE-CERT-INSTALLED? -- both C )
( primitives, read-only; the seed has no FORTH access at all. )
( ZUSE-ELIGIBILITY-ADD denied by default -- see block 4017. )
0 ['] ZUSE-ELIGIBILITY-ADD ACL-ALLOW!
Block 4017
( ACL-ZUSE-BOOT ( -- ) re-invokable: capsule_zuse_boot.c )
@@ -23,6 +25,8 @@ Block 4017
: ACL-ZUSE-BOOT ( -- )
ZUSE-CERT-INSTALLED? IF
ZUSE-AUTHENTICATE
1 ['] ZUSE-ELIGIBILITY-ADD ACL-ALLOW!
['] ZUSE-ELIGIBILITY-ADD ACL-PIN
LOG-INFO" zuse: activated"
ELSE
LOG-INFO" zuse: NOT activated -- no cert installed"
+787
View File
@@ -0,0 +1,787 @@
# StarForth Primitive Word Reference
This reference covers every **C-implemented primitive** word that StarForth registers. It was built from
`admin/LithosAnanake` at commit `6302dcb` (2026-09-23). It lists only words registered in C
through `register_word()` or `vm_create_word()`. Words defined in FORTH inside capsules (`*.4th`) are out of scope.
Sources:
- `src/word_registry.c`: `register_forth79_words()` registers the core set in every VM.
- `src/word_source/*.c`: one file per module.
- `src/starkernel/capsule/mama_forth_words.c`: kernel-only Hera (Mama) and child-VM words.
- `src/starkernel/repl.c` and `src/starkernel/doe_log.c`: kernel-only REPL and DoE words.
---
## Conventions
| Item | Meaning |
|---|---|
| Cell | `cell_t` is `int64_t`, so every cell is 64 bits and signed. |
| Flag | TRUE is `-1` (all bits set) and FALSE is `0`. Words that take a flag treat any non-zero value as true. |
| `addr` | A **VM address**: a byte offset into the VM's 5 MB linear memory (`VM_MEMORY_SIZE`), not a host pointer. |
| `c-addr u` | A string given as its address and length. |
| `d`, `ud` | A double-cell number made of two cells, with the **high cell on top**. |
| `xt` | An execution token. In StarForth this is the `DictEntry*` of the word. |
| `q` | A Q48.16 fixed-point value in one cell (`1.0` = `65536`). |
| `"name"` | The word parses a name from the input stream after it. |
| `( R: ... )` | The effect on the return stack. |
| **IMM** | The word is IMMEDIATE, so it runs even while compiling. |
| **CO** | The word is compile-only and sets `vm->error` if used outside a definition. |
| **K** | The word is registered only in the kernel build (`__STARKERNEL__`). |
| **H** | The word is registered only in the hosted build (the Linux or macOS binary). |
Errors: a primitive does not throw. On stack underflow or overflow, a bad address, or division by zero it sets
`vm->error = 1` and logs a message.
Stack limits: the data stack and return stack hold 1024 cells each (`STACK_SIZE`). A word name can be at most 31
characters (`WORD_NAME_MAX`).
Shadowing: when a later module registers a name again, the newer entry wins lookups. For example, `MOD`, `/MOD`,
`*/` and `*/MOD` are registered by the arithmetic module and again by the mixed-arithmetic module, so the
mixed-arithmetic versions are the active ones.
---
## Contents
1. [Stack](#1-stack)
2. [Return stack](#2-return-stack)
3. [Memory](#3-memory)
4. [Arithmetic](#4-arithmetic)
5. [Logic and comparison](#5-logic-and-comparison)
6. [Mixed-precision arithmetic](#6-mixed-precision-arithmetic)
7. [Double-cell numbers](#7-double-cell-numbers)
8. [Number formatting and output](#8-number-formatting-and-output)
9. [Strings, parsing, and input](#9-strings-parsing-and-input)
10. [Terminal I/O](#10-terminal-io)
11. [Blocks and mass storage](#11-blocks-and-mass-storage)
12. [Dictionary space](#12-dictionary-space)
13. [Dictionary manipulation](#13-dictionary-manipulation)
14. [Vocabularies](#14-vocabularies)
15. [System](#15-system)
16. [Line editor](#16-line-editor)
17. [Defining words and the compiler](#17-defining-words-and-the-compiler)
18. [Control flow](#18-control-flow)
19. [StarForth extensions](#19-starforth-extensions)
20. [Word-level ACL](#20-word-level-acl)
21. [Physics: benchmark and diagnostics](#21-physics-benchmark-and-diagnostics)
22. [Physics: pipelining diagnostics](#22-physics-pipelining-diagnostics)
23. [Physics: freeze, heat, and decay](#23-physics-freeze-heat-and-decay)
24. [Dictionary heat optimisation](#24-dictionary-heat-optimisation)
25. [Logging](#25-logging)
26. [Q48.16 fixed-point math](#26-q4816-fixed-point-math)
27. [Inference engine (SSM, L8, and Bayes)](#27-inference-engine-ssm-l8-and-bayes)
28. [DEFER and IS](#28-defer-and-is)
29. [Framebuffer (Hestia only)](#29-framebuffer-hestia-only)
30. [Keyboard](#30-keyboard)
31. [TrueType text](#31-truetype-text)
32. [REPL scrollback](#32-repl-scrollback)
33. [Kernel REPL and DoE hooks](#33-kernel-repl-and-doe-hooks)
34. [Hera (Mama) and child-VM words](#34-hera-mama-and-child-vm-words)
35. [Hosted lifecycle stubs](#35-hosted-lifecycle-stubs)
36. [Implementation quirks to know](#36-implementation-quirks-to-know)
---
## 1. Stack
`src/word_source/stack_words.c`
| Word | Stack | Description |
|---|---|---|
| `DROP` | `( x -- )` | Discards the top cell. |
| `DUP` | `( x -- x x )` | Copies the top cell. |
| `?DUP` | `( x -- x x \| 0 -- 0 )` | Copies the top cell only when it is non-zero. The usual idiom is `?DUP IF ... THEN`. |
| `SWAP` | `( x1 x2 -- x2 x1 )` | Swaps the top two cells. |
| `OVER` | `( x1 x2 -- x1 x2 x1 )` | Copies the second cell to the top. |
| `ROT` | `( x1 x2 x3 -- x2 x3 x1 )` | Moves the third cell to the top. |
| `-ROT` | `( x1 x2 x3 -- x3 x1 x2 )` | Moves the top cell down to third place. This is the reverse of `ROT`. |
| `DEPTH` | `( -- n )` | Pushes the number of cells that were on the data stack before `DEPTH` ran. |
| `PICK` | `( xn … x0 n -- xn … x0 xn )` | Copies the n-th cell to the top. **0-based:** `0 PICK` is `DUP` and `1 PICK` is `OVER`. An error occurs if `n < 0` or `n ≥ depth`. |
| `ROLL` | `( … n -- … )` | Moves a cell to the top and closes the gap. **Non-standard:** `n` counts from the *bottom* of the stack (1-based), so `1 ROLL` moves the deepest cell to the top. `0 ROLL` does nothing. See [§36](#36-implementation-quirks-to-know). |
## 2. Return stack
`src/word_source/return_stack_words.c`
| Word | Stack | Description |
|---|---|---|
| `>R` | `( x -- ) ( R: -- x )` | Moves a cell from the data stack to the return stack. Inside a definition, balance it with `R>` before `;` or `EXIT`. |
| `R>` | `( -- x ) ( R: x -- )` | Moves a cell from the return stack back to the data stack. |
| `R@` | `( -- x ) ( R: x -- x )` | Copies the top of the return stack without removing it. |
## 3. Memory
`src/word_source/memory_words.c`. Every address is a VM byte offset and is checked against the VM's memory bounds.
| Word | Stack | Description |
|---|---|---|
| `@` | `( addr -- x )` | Fetches the cell at `addr`. |
| `!` | `( x addr -- )` | Stores `x` at `addr`. |
| `C@` | `( addr -- c )` | Fetches the byte at `addr`, zero-extended. |
| `C!` | `( c addr -- )` | Stores the low 8 bits of `c` at `addr`. |
| `+!` | `( n addr -- )` | Adds `n` to the cell at `addr`. |
| `-!` | `( n addr -- )` | Subtracts `n` from the cell at `addr`. |
| `2@` | `( addr -- x-lo x-hi )` | Fetches two cells: the low cell from `addr` and the high cell from `addr+8`, leaving the high cell on top. |
| `2!` | `( x-lo x-hi addr -- )` | Stores two cells: the low cell at `addr` and the high cell at `addr+8`. |
| `FILL` | `( addr u c -- )` | Fills `u` bytes starting at `addr` with the byte `c`. |
| `MOVE` | `( src dst u -- )` | Copies `u` bytes from `src` to `dst`. It is safe when the ranges overlap because it uses memmove semantics. |
| `ERASE` | `( addr u -- )` | Sets `u` bytes to zero. |
| `CELLS` | `( n -- n*8 )` | Scales a cell count to a byte count. |
## 4. Arithmetic
`src/word_source/arithmetic_words.c`. All arithmetic is signed 64-bit, and division truncates toward zero as in C.
| Word | Stack | Description |
|---|---|---|
| `+` | `( n1 n2 -- n1+n2 )` | Adds the two cells. |
| `-` | `( n1 n2 -- n1-n2 )` | Subtracts `n2` from `n1`. |
| `*` | `( n1 n2 -- n1*n2 )` | Multiplies the two cells. The result wraps modulo 2⁶⁴. |
| `/` | `( n1 n2 -- n1/n2 )` | Divides, truncating toward zero. Division by zero sets an error. |
| `MOD` | `( n1 n2 -- rem )` | Pushes the remainder of `n1 / n2`, which has the sign of `n1`. This name is shadowed by §6. |
| `/MOD` | `( n1 n2 -- rem quot )` | Pushes the remainder and the quotient, with the quotient on top. This name is shadowed by §6. |
| `*/` | `( n1 n2 n3 -- n1*n2/n3 )` | Multiplies and then divides using a wide intermediate. This name is shadowed by §6. |
| `*/MOD` | `( n1 n2 n3 -- rem quot )` | Like `*/`, but also leaves the remainder. This name is shadowed by §6. |
| `1+` `1-` | `( n -- n±1 )` | Increments or decrements by 1. |
| `2+` `2-` | `( n -- n±2 )` | Adds or subtracts 2. |
| `2*` | `( n -- n*2 )` | Shifts left by one bit. |
| `2/` | `( n -- n/2 )` | Shifts right by one bit, keeping the sign (arithmetic shift). |
| `ABS` | `( n -- \|n\| )` | Pushes the absolute value. |
| `NEGATE` | `( n -- -n )` | Pushes the two's-complement negation. |
| `MIN` `MAX` | `( n1 n2 -- n3 )` | Pushes the smaller or the larger value, compared as signed numbers. |
## 5. Logic and comparison
`src/word_source/logical_words.c`. Comparison words return a proper flag of `-1` or `0`.
| Word | Stack | Description |
|---|---|---|
| `AND` `OR` `XOR` | `( x1 x2 -- x3 )` | Bitwise AND, OR, and XOR. |
| `NOT` | `( x -- flag )` | **FORTH-79 logical NOT:** `0` gives `TRUE` and any other value gives `FALSE`. This is *not* a bitwise complement; use `INVERT` for that. |
| `INVERT` | `( x -- ~x )` | Bitwise complement (from FORTH-83). |
| `LSHIFT` | `( x u -- x<<u )` | Logical shift left by `u` bits. |
| `RSHIFT` | `( x u -- x>>u )` | Logical (unsigned) shift right by `u` bits. |
| `0=` | `( n -- flag )` | True if `n` is 0. |
| `0<` | `( n -- flag )` | True if `n` is negative. |
| `0>` | `( n -- flag )` | True if `n` is positive. |
| `0<>` | `( n -- flag )` | True if `n` is not 0. |
| `=` `<>` | `( n1 n2 -- flag )` | Tests for equality or inequality. |
| `<` `>` `<=` `>=` | `( n1 n2 -- flag )` | Signed comparisons of `n1` against `n2`. |
| `U<` `U>` | `( u1 u2 -- flag )` | Unsigned comparisons. |
| `WITHIN` | `( n lo hi -- flag )` | True if `lo ≤ n < hi`, using the standard half-open range. |
| `TRUE` | `( -- -1 )` | Pushes the canonical true flag. |
| `FALSE` | `( -- 0 )` | Pushes the canonical false flag. |
## 6. Mixed-precision arithmetic
`src/word_source/mixed_arithmetic_words.c`. A double here means two full 64-bit cells (128 bits).
| Word | Stack | Description |
|---|---|---|
| `M+` | `( d n -- d' )` | Adds a signed single to a double and propagates the carry into the high cell. |
| `M-` | `( d n -- d' )` | Subtracts a signed single from a double and propagates the borrow. |
| `M*` | `( n1 n2 -- d )` | Multiplies 64×64 into a full 128-bit signed product, using `__int128` internally. The result can be printed with `D.`. |
| `M/MOD` | `( d n -- rem quot )` | Divides a 128-bit double by a single, leaving the quotient on top. It uses bit-serial long division, so it works in the freestanding kernel without libgcc. |
| `MOD` | `( n1 n2 -- rem )` | **Active version.** Computes `n1 % n2`. Division by zero sets an error. |
| `/MOD` | `( n1 n2 -- rem quot )` | **Active version.** Leaves the remainder under the quotient. Division by zero sets an error. |
| `*/` | `( n1 n2 n3 -- n4 )` | **Active version.** Computes `(n1*n2)/n3` with a wide intermediate, so `n1*n2` does not overflow. Division by zero sets an error. Typical use is scaling, for example `x 355 113 */`. |
| `*/MOD` | `( n1 n2 n3 -- rem quot )` | **Active version.** Like `*/`, but also leaves the remainder under the quotient. |
## 7. Double-cell numbers
`src/word_source/double_words.c`. In every stack picture, `d` stands for the pair `( lo hi )` with the high cell on top.
| Word | Stack | Description |
|---|---|---|
| `S>D` | `( n -- d )` | Sign-extends a single to a double. |
| `D+` `D-` | `( d1 d2 -- d3 )` | Double add and subtract, with carry or borrow. |
| `DNEGATE` | `( d -- -d )` | Negates a double. |
| `DABS` | `( d -- \|d\| )` | Pushes the absolute value of a double. |
| `DMAX` `DMIN` | `( d1 d2 -- d3 )` | Pushes the larger or smaller double, compared as signed values. |
| `D<` | `( d1 d2 -- flag )` | Signed less-than on doubles. |
| `D=` | `( d1 d2 -- flag )` | Equality on doubles. |
| `D0=` | `( d -- flag )` | True if the double is zero. |
| `D0<` | `( d -- flag )` | True if the double is negative. |
| `D2*` | `( d -- d*2 )` | Shifts a double left by one bit across both cells. |
| `D2/` | `( d -- d/2 )` | Shifts a double right by one bit (arithmetic shift) across both cells. |
| `2DROP` | `( x1 x2 -- )` | Drops a cell pair. |
| `2DUP` | `( x1 x2 -- x1 x2 x1 x2 )` | Duplicates a cell pair. |
| `2SWAP` | `( p1 p2 -- p2 p1 )` | Swaps two cell pairs. |
| `2OVER` | `( p1 p2 -- p1 p2 p1 )` | Copies the second pair to the top. |
| `2ROT` | `( p1 p2 p3 -- p2 p3 p1 )` | Rotates three cell pairs. |
| `2>R` | `( x1 x2 -- ) ( R: -- x1 x2 )` | Moves a pair to the return stack. |
| `2R>` | `( -- x1 x2 ) ( R: x1 x2 -- )` | Moves a pair back from the return stack. |
| `2R@` | `( -- x1 x2 ) ( R: x1 x2 -- x1 x2 )` | Copies a pair from the return stack. |
## 8. Number formatting and output
`src/word_source/format_words.c`
| Word | Stack | Description |
|---|---|---|
| `.` | `( n -- )` | Prints a signed number in the current `BASE`, followed by a space. |
| `.R` | `( n width -- )` | Prints a signed number right-aligned in a field `width` characters wide. |
| `U.` | `( u -- )` | Prints an unsigned number followed by a space. |
| `U.R` | `( u width -- )` | Prints an unsigned number right-aligned. |
| `D.` | `( d -- )` | Prints a signed double. |
| `D.R` | `( d width -- )` | Prints a signed double right-aligned. |
| `.S` | `( -- )` | Prints the data stack without changing it. This is the main debugging aid. |
| `?` | `( addr -- )` | Prints the cell at `addr`; it is the same as `@ .`. |
| `DUMP` | `( addr u -- )` | Prints a hex and ASCII dump of `u` bytes starting at `addr`. |
| `<#` | `( -- )` | Starts pictured numeric output by resetting the conversion buffer. |
| `#` | `( ud -- ud' )` | Converts one digit (`ud mod BASE`) into the buffer. It also accepts a single signed cell and converts its magnitude. |
| `#S` | `( ud -- 0 0 )` | Converts digits until the value is zero, always producing at least one digit. It accepts a single cell the same way `#` does. |
| `HOLD` | `( c -- )` | Inserts the character `c` into the pictured output buffer. |
| `SIGN` | `( n -- )` | Inserts `-` if `n` is negative. |
| `#>` | `( ud -- c-addr u )` | Ends conversion and leaves the string. It is tolerant: it pops `ud` only if one is present. |
| `BASE` | `( -- addr )` | Pushes the address of the number-conversion radix variable. |
| `DECIMAL` `HEX` `OCTAL` | `( -- )` | Sets `BASE` to 10, 16, or 8. |
Example: `: .$ ( n -- ) <# # # 46 HOLD #S #> TYPE ;` prints `1234` as `12.34`.
## 9. Strings, parsing, and input
`src/word_source/string_words.c`. The comparison and search words below also accept a counted string in place of an
`addr u` pair; they detect it when the first byte at `addr` equals `u`.
| Word | Stack | Description |
|---|---|---|
| `COUNT` | `( c-addr1 -- c-addr2 u )` | Converts a counted string (length byte followed by characters) into an address and length. |
| `EXPECT` | `( addr u -- )` | Reads up to `u` characters from the terminal into `addr` and stores the count read in `SPAN`. |
| `SPAN` | `( -- addr )` | Pushes the address of the variable that holds the count from the last `EXPECT`. |
| `QUERY` | `( -- )` | Reads a line into `TIB` and resets `>IN`. |
| `TIB` | `( -- addr )` | Pushes the address of the terminal input buffer. |
| `>IN` | `( -- addr )` | Pushes the address of the offset into the current input source. |
| `SOURCE` | `( -- addr u )` | Pushes the current input buffer and its length. |
| `WORD` | `( c -- c-addr )` | Skips leading `c` characters, parses up to the next `c`, and returns a counted string. The usual form is `BL WORD`. |
| `BL` | `( -- 32 )` | Pushes the ASCII space character. |
| `S"` **IMM** | `( "ccc<">" -- c-addr u )` | In interpret mode, stores the string at `HERE` and pushes it. When compiling, it compiles `(s")` followed by the inline text. |
| `(s")` | `( -- c-addr u )` | Runtime for a compiled `S"`: reads the inline `[len][chars][pad]` block and skips the IP past it. The compiler inserts it; you do not call it directly. |
| `[']` **IMM** | `( "name" -- xt )` | While compiling, compiles the xt of `name` as a literal. In interpret mode it behaves like `'`. |
| `LITERAL` `[LITERAL]` | – | Placeholders that do nothing. `LITERAL` is re-registered in §17 (the working version); `[LITERAL]` has no replacement and still does nothing. |
| `CONVERT` | `( d1 addr1 -- d2 addr2 )` | Accumulates the digits at `addr1+1…` into `d1` and stops at the first non-digit. This is a simplified version. |
| `NUMBER` | `( c-addr -- n flag )` | Converts a counted string to a number. Only base 10 is supported, and `flag` shows whether it succeeded. |
| `ENCLOSE` | `( addr c -- addr n1 n2 n3 )` | The classic FIG parser: gives the offsets of the start of the token, the delimiter after it, and the next character. |
| `-TRAILING` | `( addr u -- addr u' )` | Removes trailing spaces from the length. |
| `CMOVE` | `( src dst u -- )` | Copies bytes upward from low to high addresses. It is safe for overlapping ranges when `dst ≤ src`. |
| `CMOVE>` | `( src dst u -- )` | Copies bytes downward from high to low addresses. It is safe for overlapping ranges when `dst > src`. |
| `COMPARE` | `( a1 u1 a2 u2 -- n )` | Compares two strings case-sensitively and returns `-1`, `0`, or `1`. |
| `SEARCH` | `( a1 u1 a2 u2 -- a3 u3 flag )` | Finds string 2 inside string 1. If found, it returns the tail starting at the match and `-1`. If not, it returns string 1 unchanged and `0`. |
| `SCAN` | `( addr u c -- addr' u' )` | Advances to the first occurrence of `c`. If there is none, it returns the end of the string and `0`. |
| `SKIP` | `( addr u c -- addr' u' )` | Skips leading occurrences of `c`. |
| `BLANK` | `( addr u -- )` | Fills `u` bytes with spaces. |
## 10. Terminal I/O
`src/word_source/io_words.c`
| Word | Stack | Description |
|---|---|---|
| `EMIT` | `( c -- )` | Prints one character. |
| `CR` | `( -- )` | Prints a newline. |
| `KEY` | `( -- c )` | Waits for a character and pushes it. |
| `?TERMINAL` | `( -- flag )` | True if a key is waiting. It does not block. |
| `TYPE` | `( c-addr u -- )` | Prints `u` characters. |
| `SPACE` | `( -- )` | Prints one space. |
| `SPACES` | `( n -- )` | Prints `n` spaces. |
| `."` **IMM** | `( "ccc<">" -- )` | In interpret mode, prints the string immediately. When compiling, it compiles `(do-string)` and the inline text. |
| `(do-string)` | `( -- )` | Runtime for a compiled `."`: prints the inline string and skips the IP past it. The compiler inserts it; you do not call it directly. |
## 11. Blocks and mass storage
`src/word_source/block_words.c`. A block is 1024 bytes, viewed as 16 lines of 64 characters. Block numbers are
LBNs (logical block numbers) in one address space that spans every attached device.
| Word | Stack | Description |
|---|---|---|
| `BLOCK` | `( u -- addr )` | Returns the VM address of the buffer holding block `u`, reading it from disk if needed. It does not mark the buffer dirty. |
| `BUFFER` | `( u -- addr )` | Assigns a buffer to block `u` *without* reading from disk and marks it dirty. Use it when you will overwrite the whole block. |
| `UPDATE` | `( -- )` | Marks the current (`SCR`) block dirty and syncs it to the C block layer. |
| `SAVE-BUFFERS` | `( -- )` | Writes every dirty buffer to disk. |
| `EMPTY-BUFFERS` | `( -- )` | Discards every buffer **without** writing it and zeroes the user block window. |
| `FLUSH` | `( -- )` | Runs `SAVE-BUFFERS` and then invalidates all buffers. |
| `LOAD` | `( u -- )` | Sets `SCR` to `u` and interprets the 1024 bytes of block `u` as FORTH source. Block 0 cannot be loaded. |
| `THRU` | `( u1 u2 -- )` | Loads blocks `u1` through `u2`, including both ends. |
| `-->` | `( -- )` | Inside a block being loaded, continues interpreting at the next block. |
| `LIST` | `( u -- )` | Sets `SCR` to `u` and prints the block. |
| `SCR` | `( -- addr )` | Pushes the address of the variable holding the block number last listed or loaded. |
| `BLK-CONFIRM-FORMAT` | `( lbn -- )` | Commits the container format of the device that owns `lbn`. Until this has run, the block layer **refuses every write** to that device. Only the owner (for example Artemis) should call it, and only after checking the disk contents are safe to touch. |
| `RELOCATE-BLOCK` | `( home target -- )` | Moves the contents of `home` to `target` and redirects all later access to `home` through `target`. It is a mechanical primitive: it does not check whether `target` is free or owned by the caller. |
| `BLK-ACL-ALLOW@` | `( blk -- allow )` | Reads the cached allow/deny flag of a block's ACL. |
| `BLK-ACL-ALLOW!` | `( allow blk -- )` | Sets a block's cached allow/deny flag. |
| `BLK-ACL-TTL@` | `( blk -- ttl )` | Reads a block's ACL TTL countdown. |
| `BLK-ACL-TTL!` | `( ttl blk -- )` | Sets a block's ACL TTL countdown. |
| `BLK-OWNER@` | `( blk -- fp )` | Pushes the 8-byte owner fingerprint as the raw bits of one cell. There is no `BLK-OWNER!`: ownership is set only in C, during MINT or birth. |
| `BLK-ATTACH` | `( dev-ptr -- ok? )` | Registers an already-open `blkio_dev_t*`, passed as a raw pointer cell, in the unified LBN space. The pointer is trusted without checks. Artemis's USB-attach handler uses it. |
## 12. Dictionary space
`src/word_source/dictionary_words.c`
| Word | Stack | Description |
|---|---|---|
| `HERE` | `( -- addr )` | Pushes the next free byte in the dictionary. |
| `ALIGN` | `( -- )` | Rounds `HERE` up to the next 8-byte cell boundary. |
| `ALLOT` | `( n -- )` | Reserves `n` bytes at `HERE`. A negative `n` gives space back. |
| `,` | `( x -- )` | Compiles one cell at `HERE` and advances `HERE`. |
| `C,` | `( c -- )` | Compiles one byte. |
| `2,` | `( x-lo x-hi -- )` | Compiles two cells, low cell first. |
| `PAD` | `( -- addr )` | Pushes the address of a 512-byte scratch buffer near the top of memory. It is safe for temporary strings. |
| `SP@` | `( -- n )` | Pushes the data stack pointer *index* (`dsp`). An empty stack gives `-1` and one item gives `0`. |
| `SP!` | `( n -- )` | Restores the stack pointer index. It can only shrink the stack, never grow it. |
| `LATEST` | `( -- addr )` | Pushes the address of the most recent definition. |
## 13. Dictionary manipulation
`src/word_source/dictionary_manipulation_words.c`. Header-field words work on raw header addresses. They exist for
FIG and FORTH-79 compatibility; take care with them.
| Word | Stack | Description |
|---|---|---|
| `'` | `( "name" -- xt )` | Parses `name` and pushes its xt. It is not IMMEDIATE; inside a definition, use `[']`. |
| `FIND` | `( "name" -- xt \| 0 )` | **Parses from the input stream**, not from a counted string on the stack. It pushes the entry, or `0` if the word is not found (a miss is not an error). For a counted string already in memory, use `(FIND)` from §14. |
| `SMUDGE` | `( -- )` | **CO.** Toggles the smudge (hidden) bit on the latest word. |
| `HIDDEN` | `( -- )` | **CO.** Sets the latest word's hidden bit (unlike `SMUDGE`, it does not toggle). |
| `>BODY` | `( xt -- addr )` | Pushes the address of the parameter (data) field. |
| `>NAME` | `( xt -- nfa )` | Pushes the name field. |
| `NAME>` | `( nfa -- xt )` | Goes from the name field to the xt. |
| `>LINK` | `( xt -- lfa )` | Pushes the link field. |
| `LINK>` | `( lfa -- xt )` | Follows the link to the next (older) word. |
| `CFA` `LFA` `NFA` `PFA` | `( addr -- addr' )` | FIG-style field-address conversions (code, link, name, and parameter fields). |
| `TRAVERSE` | `( addr n -- addr' )` | Moves across a name field forward (`n=1`) or backward (`n=-1`). |
| `INTERPRET` | `( -- )` | Runs the text interpreter on the rest of the current input. |
## 14. Vocabularies
`src/word_source/vocabulary_words.c`
| Word | Stack | Description |
|---|---|---|
| `VOCABULARY` | `( "name" -- )` | Creates a vocabulary. Running `name` later makes it the `CONTEXT` (search) vocabulary. |
| `DEFINITIONS` | `( -- )` | Sets `CURRENT` to `CONTEXT`, so new definitions go into the vocabulary being searched. |
| `CONTEXT` | `( -- addr )` | Pushes the address of the search-vocabulary pointer. |
| `CURRENT` | `( -- addr )` | Pushes the address of the definition-vocabulary pointer. |
| `FORTH` | `( -- )` | Makes the root `FORTH` vocabulary the context. |
| `ORDER` | `( -- )` | Prints the search order (`CONTEXT`, then `FORTH`) and `CURRENT`. |
| `(FIND)` | `( c-addr -- c-addr 0 \| xt 1 \| xt -1 )` | Looks up a counted string in `CONTEXT` and then in `FORTH`. It returns `1` for an IMMEDIATE word, `-1` for a normal word, and `0` if not found. |
Example: `VOCABULARY GRAPHICS GRAPHICS DEFINITIONS : BOX ... ; FORTH DEFINITIONS`
## 15. System
`src/word_source/system_words.c`
| Word | Stack | Description |
|---|---|---|
| `(` **IMM** | `( "ccc<)>" -- )` | Starts a comment that runs to the closing `)`. |
| `\` **IMM** | `( "ccc<eol>" -- )` | Starts a comment that runs to the end of the line. |
| `EXECUTE` | `( xt -- )` | Runs the word identified by `xt`. |
| `NOP` | `( -- )` | Does nothing. |
| `QUIT` **IMM** | `( -- ) ( R: … -- )` | Clears the return stack and the error flag and returns to the outer interpreter. The data stack is kept. It is refused (sets an error) inside a definition. |
| `ABORT` | `( … -- )` | Clears both stacks and returns to the interpreter. It is **not** reported as an error. |
| `ABORT"` **IMM** | `( flag "ccc<">" -- )` | If `flag` is non-zero, prints the message and runs `ABORT`. It works in both interpret and compile mode. |
| `(ABORT")` | `( flag addr u -- )` | Runtime for a compiled `ABORT"`. The compiler inserts it; you do not call it directly. |
| `COLD` | `( -- )` | Clears both stacks and the error flag, returns to interpret mode, and moves `HERE` back to 1024 if it is higher. Dictionary headers are **not** removed; this is a minimal cold start. |
| `WARM` | `( -- )` | Clears both stacks and the error flag and returns to interpret mode. `HERE` and the dictionary are kept. |
| `BYE` | `( -- )` | Leaves this VM. In a child VM it halts the VM and returns to the parent's REPL. On Hera, the kernel's `BYE` from §34 takes precedence. |
| `REBOOT` | `( c-addr u -- )` | Sets the boot arguments and does a cold reset. Interpret mode only. |
| `SAVE-SYSTEM` | `( -- )` | Takes a simple snapshot of the start of VM memory. |
| `WORDS` | `( -- )` | Lists the words in the current vocabulary. |
| `VLIST` | `( -- )` | Gives a detailed word listing. |
| `SEE` | `( "name" -- )` | Decompiles and shows a definition. |
| `PAGE` | `( -- )` | Clears the screen. |
| `79-STANDARD` | `( -- flag )` | Pushes `-1` when FORTH-79 compliance mode is on. |
## 16. Line editor
`src/word_source/editor_words.c`. The editor works on block `SCR` as 16 lines of 64 characters.
| Word | Stack | Description |
|---|---|---|
| `L` | `( u -- )` | Prints line `u` (0–15) of the current screen. |
| `S` | `( c-addr len u -- )` | Replaces line `u` with the string, padding with spaces or truncating to 64 characters. |
| `SHOW` | `( -- )` | Prints the whole screen with line numbers. |
| `EDIT` | `( u -- )` | Opens a minimal stdin/stdout line-editor shell on block `u`. |
## 17. Defining words and the compiler
`src/word_source/defining_words.c`
| Word | Stack | Description |
|---|---|---|
| `:` **IMM** | `( "name" -- )` | Starts a colon definition and switches to compile mode. The new word stays hidden until `;`. |
| `;` **IMM** | `( -- )` | Compiles `EXIT`, ends the definition, reveals the word, and returns to interpret mode. |
| `CREATE` | `( "name" -- )` | Makes a header whose runtime pushes its data-field address (`HERE` aligned to a cell). It allocates **no** space, so follow it with `ALLOT` or `,`. |
| `VARIABLE` | `( "name" -- )` | Makes a word that pushes the address of one newly allocated cell. |
| `CONSTANT` | `( x "name" -- )` | Makes a word that pushes `x`. |
| `DOES>` **IMM** | `( -- )` | Inside a defining word, ends the create part. Words later made by that defining word run the code after `DOES>` with their body address on the stack. |
| `IMMEDIATE` **IMM** | `( -- )` | Marks the latest definition IMMEDIATE. |
| `STATE` | `( -- addr )` | Pushes the address of the compile-state cell (0 means interpreting). |
| `[` **IMM** | `( -- )` | Switches to interpret mode inside a definition. |
| `]` **IMM** | `( -- )` | Switches to compile mode. |
| `LITERAL` **IMM** | `( x -- )` | Compiles `x` so that it is pushed at runtime. Typical use: `[ 6 7 * ] LITERAL`. |
| `LIT` | `( -- x )` | Runtime for literals: pushes the next inline cell. The compiler inserts it; you do not call it directly. |
| `COMPILE` **IMM** | `( "name" -- )` | Legacy form: compiles a call to `name`. |
| `[COMPILE]` **IMM** | `( "name" -- )` | Compiles `name` even when it is IMMEDIATE. |
| `FORGET` | `( "name" -- )` | Removes `name` and every newer word and moves `HERE` back. Words below `FENCE` cannot be forgotten. |
| `FENCE` | `( -- )` | Moves the `FORGET` boundary up to the current top of the dictionary. A capsule calls it after loading to protect its own words. |
| `does_rt` | – | Internal `DOES>` helper that switches the new child word to DODOES. It is registered only so the threaded code can refer to it; do not call it. |
Example: `: ARRAY ( n "name" -- ) CREATE CELLS ALLOT DOES> ( i -- addr ) SWAP CELLS + ;`
## 18. Control flow
`src/word_source/control_words.c`. Every structure word is **IMM** and **CO**. Branch offsets are in bytes. Up to 64
structures can be nested at compile time (`CF_STACK_MAX`).
| Word | Stack | Description |
|---|---|---|
| `IF` | `( flag -- )` | Runs the following code only if `flag` is non-zero. It compiles `(0BRANCH)`. |
| `ELSE` | `( -- )` | Starts the code that runs when the `IF` flag was zero. |
| `THEN` | `( -- )` | Ends an `IF` or `IF … ELSE` structure. |
| `BEGIN` | `( -- )` | Marks the start of a loop. |
| `UNTIL` | `( flag -- )` | Loops back to `BEGIN` while `flag` is zero. |
| `AGAIN` | `( -- )` | Loops back to `BEGIN` unconditionally. Leave with `EXIT` or `ABORT`. |
| `WHILE` | `( flag -- )` | In `BEGIN … WHILE … REPEAT`, leaves the loop when `flag` is zero. |
| `REPEAT` | `( -- )` | Jumps back to `BEGIN` and resolves the exit of `WHILE`. |
| `DO` | `( limit start -- ) ( R: -- limit index )` | Starts a counted loop that always runs at least once. |
| `?DO` | `( limit start -- )` | Like `DO`, but skips the loop body when `start = limit`. |
| `LOOP` | `( -- )` | Adds 1 to the index and loops while `index < limit`. |
| `+LOOP` | `( n -- )` | Adds `n` to the index. For `n ≥ 0` it continues while `index < limit`; for `n < 0` it continues while `index ≥ limit`. |
| `LEAVE` | `( -- )` | Exits the innermost `DO` loop immediately: it sets the index to the limit and jumps past `LOOP`. |
| `I` | `( -- index )` | Pushes the index of the innermost loop. It is not IMMEDIATE. |
| `J` | `( -- index )` | Pushes the index of the next outer loop. |
| `UNLOOP` | `( -- ) ( R: limit index -- )` | Drops the loop parameters. Use it before `EXIT` inside a `DO` loop. |
| `EXIT` | `( -- )` | Returns from the current colon definition. Using it in interpret mode is an error. |
| `CASE` | `( x -- x )` | Starts a case structure. |
| `OF` | `( x v -- \| x )` | If `x = v`, drops both and runs the clause; otherwise keeps `x` and skips to the next `OF`. |
| `ENDOF` | `( -- )` | Ends an `OF` clause and jumps to `ENDCASE`. |
| `ENDCASE` | `( x -- )` | Drops the selector and resolves every `ENDOF` jump. |
| `(BRANCH)` | `( -- )` | Runtime: unconditional relative branch. The compiler inserts it; you do not call it directly. |
| `(0BRANCH)` | `( flag -- )` | Runtime: branches when `flag` is 0. The compiler inserts it; you do not call it directly. |
| `(DO)` `(?DO)` | `( limit start -- )` | Runtime for loop entry. The compiler inserts them; you do not call them directly. |
| `(LOOP)` `(+LOOP)` | `( -- )` / `( n -- )` | Runtime for loop increment and test. The compiler inserts them; you do not call them directly. |
| `(LEAVE)` | `( -- )` | Runtime for `LEAVE`: sets index to limit. The compiler inserts it; you do not call it directly. |
Examples:
```forth
: COUNTDOWN ( n -- ) BEGIN DUP . 1- DUP 0= UNTIL DROP ;
: TABLE ( -- ) 5 0 DO 5 0 DO I J * 4 .R LOOP CR LOOP ;
: COLOR ( n -- ) CASE 0 OF ." red" ENDOF 1 OF ." green" ENDOF ." ?" ENDCASE ;
```
## 19. StarForth extensions
`src/word_source/starforth_words.c`. These words are registered in both `FORTH` and the `STARFORTH` vocabulary.
| Word | Stack | Description |
|---|---|---|
| `ENTROPY@` | `( xt -- n )` | Pushes the `execution_heat` counter of a word. The name says "entropy", but the value is execution heat. Registered only in the `STARFORTH` vocabulary. |
| `ENTROPY!` | `( n xt -- )` | Sets a word's `execution_heat` counter. Registered only in the `STARFORTH` vocabulary. |
| `WORD-ENTROPY` | `( -- )` | Prints the execution heat of every word. |
| `RESET-ENTROPY` | `( -- )` | Sets every heat counter to zero. |
| `TOP-WORDS` | `( n -- )` | Prints the `n` hottest words. |
| `(-` | `( "ccc<)>" -- )` | A comment that marks metadata blocks to extract into `init.4th`. It consumes input up to `)`. |
| `INIT` | `( -- )` | Reads `./capsules/core/init.4th`, copies its blocks from block 1 onward, and runs them. |
| `VERSION` | `( -- )` | Prints `StarForth v<ver> <arch> <variant> <timestamp>`. |
| `SEED` | `( n -- )` | Seeds the PRNG so random sequences can be reproduced. |
| `RANDOM` | `( lo hi -- n )` | Pushes a pseudo-random number in `[lo, hi]`, including both ends. |
| `WAIT` | `( n -- )` | Waits `n` heartbeat ticks by calling `vm_tick()` `n` times. It counts heartbeats, not wall-clock time, so it behaves the same on amd64, aarch64, and riscv64. |
| `HEARTBEAT-TICKS@` | `( -- n )` | Pushes the canonical heartbeat tick count (Loop #7). The project uses this as its clock. It is read-only. |
| `ZUSE-AUTHENTICATE` | `( -- )` | Sets `zuse_session = 1`. The write happens only in C. |
| `ZUSE-SESSION?` | `( -- flag )` | True if `ZUSE-AUTHENTICATE` has run during this boot. It is read-only. |
| `ZUSE-PUBKEY@` | `( i -- u )` | Pushes 8-byte little-endian chunk `i` (0–3) of Zuse's Ed25519 **public** key. An out-of-range `i` pushes 0 and sets an error. The private seed is never exposed. |
| `ZUSE-CERT-INSTALLED?` | `( -- flag )` | True once the one-time certificate fuse has been blown. |
## 20. Word-level ACL
`src/word_source/acl_words.c`. Every word takes an `xt`, obtained with `'` or `[']`. Writes are silently ignored for
a **pinned** word.
| Word | Stack | Description |
|---|---|---|
| `ACL-MODE@` | `( xt -- mode )` | Pushes the enforcement mode: 0 is STRICT (the decision is permanent) and 1 is TTL (the decision is rechecked when the countdown expires). |
| `ACL-MODE!` | `( mode xt -- )` | Sets the enforcement mode. |
| `ACL-TTL@` | `( xt -- n )` | Pushes the TTL countdown. At 0 in TTL mode, the interpreter calls `acl_recheck()`. |
| `ACL-TTL!` | `( n xt -- )` | Sets the TTL, clamped to `[0, UINT32_MAX]`. |
| `ACL-ALLOW@` | `( xt -- flag )` | Pushes the cached decision: `-1` means allowed and `0` means denied. |
| `ACL-ALLOW!` | `( flag xt -- )` | Sets the cached decision; any non-zero value means allowed. |
| `ACL-PINNED?` | `( xt -- flag )` | True if the ACL fields are pinned and therefore immutable. |
| `ACL-PIN` | `( xt -- )` | Pins the word. **This is one-way**: no FORTH word can unpin it. |
| `ACL-HEAT@` | `( xt -- heat )` | Pushes the execution heat. `ACL.4th` uses it to calibrate TTLs. |
| `ACL-WORD-ID` | `( xt -- id )` | Pushes the word's stable `word_id`. It never changes, so it is safe to use as a table index. |
| `ACL-INHERIT` | `( src-xt dst-xt -- )` | Copies the mode from `src` to `dst` and resets `dst`: unpinned, TTL 0, allowed. It is written in C because only C may clear a pin. |
| `ACL-INIT-PRIMITIVES` | `( -- )` | For every unpinned word, sets TTL to 0, allow to 1, and mode to TTL. `ACL-BOOT` calls it. |
## 21. Physics: benchmark and diagnostics
`src/word_source/physics_benchmark_words.c`. These are interactive diagnostics that print to the console.
| Word | Stack | Description |
|---|---|---|
| `BENCH-DICT-LOOKUP` | `( iterations -- )` | Benchmarks dictionary lookup and records Q48.16 latencies. Use at least 10,000 iterations; 100,000 is the standard run and 1,000,000 is a stress test. |
| `PHYSICS-CACHE-STATS` | `( -- )` | Prints hot-words cache statistics. |
| `PHYSICS-TOGGLE-CACHE` | `( -- )` | Turns the hot-words cache on or off, for A/B testing. |
| `PHYSICS-RESET-STATS` | `( -- )` | Resets the cache statistics. |
| `PHYSICS-BUILD-INFO` | `( -- )` | Prints the variant's build configuration. |
| `PHYSICS-BAYESIAN-REPORT` | `( addr -- )` | Prints a Bayesian comparison of the current cache statistics against the baseline stored at `addr`. |
> `physics_diagnostic_words.c` also defines `PHYSICS-WORD-METRICS`, `PHYSICS-CALC-KNOBS`, `PHYSICS-BURN ( n -- )`
> and `PHYSICS-SHOW-FEEDBACK`, but nothing calls `register_physics_diagnostic_words()`, so **none of them are in
> the dictionary** at this commit.
## 22. Physics: pipelining diagnostics
`src/word_source/physics_pipelining_diagnostic_words.c`
| Word | Stack | Description |
|---|---|---|
| `PIPELINING-SHOW-STATS` | `( "name" -- )` | Prints the word-to-word transition metrics of `name`. |
| `PIPELINING-SHOW-TOP-TRANSITIONS` | `( "name" n -- )` | Prints the `n` words that most often follow `name`. |
| `PIPELINING-ANALYZE-WORD` | `( "name" -- )` | Prints a full analysis of one word's transitions, with hints for reading them. |
| `PIPELINING-STATS` | `( -- )` | Prints pipelining statistics aggregated across the whole dictionary. |
| `PIPELINING-RESET-ALL` | `( -- )` | Clears all transition metrics. |
| `PIPELINING-ENABLE` | `( -- )` | Placeholder. Pipelining is switched on or off at compile time. |
## 23. Physics: freeze, heat, and decay
`src/word_source/physics_freeze_words.c`. Words are named by `c-addr u` strings, for example `S" DUP" HEAT@`. An
unknown name is not an error.
| Word | Stack | Description |
|---|---|---|
| `FREEZE-WORD` | `( c-addr u -- )` | Sets `WORD_FROZEN` on the word, so Loop #3 decay stops lowering its heat. |
| `UNFREEZE-WORD` | `( c-addr u -- )` | Clears `WORD_FROZEN` and leaves `WORD_PINNED` alone. |
| `FROZEN?` | `( c-addr u -- flag )` | True if the word is frozen. An unknown word gives `0`. |
| `HEAT!` | `( heat c-addr u -- )` | Writes `execution_heat` directly, bypassing Loops #1 and #3. For testing only. |
| `HEAT@` | `( c-addr u -- heat )` | Reads `execution_heat`. An unknown word gives `0`. |
| `SHOW-HEAT` | `( c-addr u -- )` | Prints `NAME: HEAT (frozen) (pinned)`. |
| `ALL-HEATS` | `( -- )` | Prints up to 1024 words sorted by heat, hottest first. |
| `DECAY-RATE@` | `( -- q )` | Pushes the base decay rate per µs (`DECAY_RATE_PER_US_Q16`) in Q48.16. |
| `FREEZE-CRITICAL` | `( -- )` | Freezes 21 core words: `DUP DROP SWAP OVER ROT @ ! C@ C! EXECUTE IF THEN ELSE DO LOOP BEGIN UNTIL REPEAT . EMIT CR`. |
## 24. Dictionary heat optimisation
`src/word_source/dictionary_heat_diagnostic_words.c`
| Word | Stack | Description |
|---|---|---|
| `HEAT-PERCENTILES` | `( -- p25 p50 p75 )` | Pushes the current heat percentile thresholds, with the 75th on top. |
| `LOOKUP-STRATEGY@` | `( -- n )` | Pushes the lookup strategy: 0 is naive (newest-first linear scan) and 1 is heat-aware (hot bucket first). |
| `LOOKUP-STRATEGY!` | `( n -- )` | Sets the strategy. Only 0 or 1 is accepted; other values are silently ignored. |
| `REORG-BUCKETS` | `( -- )` | Re-sorts the lookup buckets by heat and refreshes the percentiles immediately, without waiting for the heartbeat. |
| `SHOW-HEAT-OPTIMIZATION` | `( -- )` | Prints the strategy, the percentiles, and the hot, warm, and cool zones. |
| `COMPARE-LOOKUPS` | `( iterations -- )` | Benchmarks naive against heat-aware lookup and prints the speedup. It restores the original strategy afterwards. |
## 25. Logging
`src/word_source/log_words.c`
| Word | Stack | Description |
|---|---|---|
| `LOG-ERROR` `LOG-WARN` `LOG-INFO` `LOG-TEST` `LOG-DEBUG` | `( -- level )` | Push the log level constants. |
| `LOG-LEVEL!` | `( level -- )` | Sets the active log filter, clamped to `[LOG-ERROR, LOG-DEBUG]`. |
| `LOG-LEVEL@` | `( -- level )` | Pushes the current log level. |
| `LOG-ERROR"` `LOG-WARN"` `LOG-INFO"` `LOG-TEST"` `LOG-DEBUG"` **IMM** | `( "ccc<">" -- )` | Log a literal string at that level. In interpret mode the string is logged immediately; when compiling, a runtime word and the inline string are compiled. |
| `LOG-ERROR-STR` `LOG-WARN-STR` `LOG-INFO-STR` `LOG-TEST-STR` `LOG-DEBUG-STR` | `( c-addr u -- )` | Log a string taken from the stack at that level. |
| `(do-log-error)` `(do-log-warn)` `(do-log-info)` `(do-log-test)` `(do-log-debug)` | `( -- )` | Runtimes for the compiled `LOG-*"` words. The compiler inserts them; you do not call them directly. |
| `(LOG-APPEND-RAW)` **K** | `( level timestamp c-addr u -- )` | Appends a raw entry to the kernel log ring, attributed to the calling VM (or `HADES`). It reports errors on the console, not through `log_message`, to avoid recursion. |
Example: `: CHECK ( n -- ) 0< IF LOG-WARN" negative input" THEN ;`
## 26. Q48.16 fixed-point math
`src/word_source/q48_words.c`. A value is `n × 65536`. The underlying type is **unsigned** `uint64_t`; see
[§36](#36-implementation-quirks-to-know) for what that means for negative values.
| Word | Stack | Description |
|---|---|---|
| `Q.+` `Q.-` | `( q1 q2 -- q3 )` | Add and subtract. |
| `Q.*` | `( q1 q2 -- q3 )` | Multiplies, computing `(a*b) >> 16`. |
| `Q./` | `( q1 q2 -- q3 )` | Divides, computing `(a << 16) / b`. **Division by zero returns 0** and sets no error. |
| `Q.ABS` | `( q -- \|q\| )` | Absolute value, treating the top bit as a sign bit. |
| `Q.NEG` | `( q -- -q )` | Two's-complement negation. |
| `Q.LOG` | `( q -- ln q )` | Natural logarithm by Newton-Raphson. Requires `q > 0`. |
| `Q.EXP` | `( q -- e^q )` | Exponential by Taylor series. |
| `Q.SQRT` | `( q -- √q )` | Square root by Newton-Raphson. |
| `Q.SIN` `Q.COS` | `( q -- q' )` | Sine and cosine of an angle in radians. The argument is reduced to [-π, π] and then a Taylor series is applied. |
| `Q.FROM-INT` | `( n -- q )` | Converts an integer to Q48.16 as `n << 16`. **A negative `n` becomes 0.** |
| `Q.TO-INT` | `( q -- n )` | Converts to an integer as `q >> 16`, truncating. |
| `Q.1` | `( -- 65536 )` | Pushes 1.0. |
| `Q.0` | `( -- 0 )` | Pushes 0.0. |
| `Q.SCALE` | `( -- 65536 )` | Pushes the scale factor; the same value as `Q.1`. |
| `Q.=` | `( q1 q2 -- flag )` | Equality. |
| `Q.<` `Q.>` | `( q1 q2 -- flag )` | Comparison, done **unsigned**. |
| `Q.0=` | `( q -- flag )` | True if the value is zero. |
| `Q.MAX` `Q.MIN` | `( q1 q2 -- q3 )` | Maximum and minimum, compared **unsigned**. |
| `Q.PRINT` | `( q -- )` | Prints the value as `int.fffff ` with five fractional digits. |
Example: `3 Q.FROM-INT Q.SQRT Q.PRINT` prints approximately `1.732`.
## 27. Inference engine (SSM, L8, and Bayes)
`src/word_source/inference_words.c`
| Word | Stack | Description |
|---|---|---|
| `INFER-RUN` | `( -- )` | Runs the full inference engine on this VM's rolling window and dictionary heat, and caches the results. |
| `INFER-WINDOW@` | `( -- u )` | Pushes the last inferred optimal window width. |
| `INFER-DECAY@` | `( -- q )` | Pushes the last inferred decay slope. |
| `INFER-VARIANCE@` | `( -- q )` | Pushes the last inferred variance. |
| `INFER-FIT@` | `( -- q )` | Pushes the last fit quality. |
| `INFER-EARLY-EXIT@` | `( -- flag )` | True if the last run exited early. |
| `Q.VARIANCE` | `( addr u -- q )` | Pushes the variance of `u` uint64 cells at `addr`. |
| `INFER-DECAY-SLOPE` | `( addr u -- q )` | Fits a decay slope to the array by linear regression. |
| `INFER-WINDOW-WIDTH` | `( addr u -- n )` | Computes the optimal window width for the array. |
| `WINDOW-DIVERSITY` | `( -- u )` | Pushes the number of distinct words in the rolling window. |
| `L8-MODE` | `( -- n )` | Pushes the current L8 (legacy 16-mode) Jacquard selection. |
| `L8-UPDATE` | `( entropy cv temporal stability -- )` | Feeds four Q48.16 metrics to `ssm_l8_update()`. `stability` is on top of the stack. |
| `L8-APPLY` | `( -- )` | Applies the currently selected L8 mode. |
| `L8-TABLE-FORCE` | `( idx -- )` | Forces the adaptive 128-config table to `idx & 127` as though the UCB bandit had picked it, and applies it. Unlike `L8-UPDATE` and `L8-APPLY`, this choice is not overwritten at the next heartbeat trial. Use it for DoE campaigns. |
| `BAYES-CACHE-MEAN` `BAYES-CACHE-LOWER` `BAYES-CACHE-UPPER` | `( -- q )` | Push the posterior mean latency and the 95 % credible bounds for hot-words cache hits. |
| `BAYES-BUCKET-MEAN` `BAYES-BUCKET-LOWER` `BAYES-BUCKET-UPPER` | `( -- q )` | Push the same three values for bucket searches. |
## 28. DEFER and IS
`src/word_source/defer_words.c`
| Word | Stack | Description |
|---|---|---|
| `DEFER` | `( "name" -- )` | Creates a vectored word. Running it before an action has been set with `IS` sets `vm->error`. |
| `IS` | `( xt "name" -- )` | Sets the action of `name`. It is refused unless `name` was created by `DEFER`. |
| `DEFER@` | `( "name" -- xt )` | Pushes the current action of a deferred word. |
Example: `DEFER GREET : HI ." hi" ; ' HI IS GREET GREET`
## 29. Framebuffer (Hestia only)
`src/word_source/framebuffer_words.c`. These words are registered only in the Hestia VM, by `capsule_birth_baby()`,
and not by `register_forth79_words()`.
| Word | Stack | Description |
|---|---|---|
| `PLOT` | `( x y color -- )` | Writes one raw pixel. The origin is top-left and Y increases downward. |
| `FB-WIDTH` | `( -- n )` | Pushes the framebuffer width in pixels. |
| `FB-HEIGHT` | `( -- n )` | Pushes the framebuffer height in pixels. |
## 30. Keyboard
`src/word_source/keyboard_words.c`. These are diagnostics for the console fabric. On a platform without the device,
each word pushes `0`.
| Word | Stack | Description |
|---|---|---|
| `KBD-SCAN` | `( -- sc -1 \| 0 )` | amd64 i8042: pops one raw XT scancode from the queue, if there is one. |
| `KBD-DEBUG` | `( -- isr spurious )` | amd64: pushes the i8042 interrupt count and the spurious-interrupt count. |
| `VKBD-EVENT` | `( -- code value -1 \| 0 )` | riscv64 and aarch64 virtio-input: pops one Linux-style `EV_KEY` code and value. |
| `VKBD-DEBUG` | `( -- isr )` | riscv64 and aarch64: pushes the virtio-input interrupt count. |
| `KEY-EVENT` | `( -- keycode pressed -1 \| 0 )` | The unified event on every architecture: pops one keycode with its pressed (1) or released (0) state. |
| `ALT+TAB` | `( -- )` | Switches the console between text and graphics, the same as the physical Alt+Tab. |
## 31. TrueType text
`src/word_source/ttf_words.c`
| Word | Stack | Description |
|---|---|---|
| `TTF-TEXT` **K** | `( c-addr u x y size color -- )` | Draws the string with the TrueType renderer at pixel `(x, y)` in the given size and color. |
## 32. REPL scrollback
`src/word_source/scroll_words.c`
| Word | Stack | Description |
|---|---|---|
| `SCROLL-BACK` **K** | `( n -- )` | Scrolls the REPL view back `n` lines. |
| `SCROLL-FWD` **K** | `( n -- )` | Scrolls the REPL view forward `n` lines, toward the live output. |
## 33. Kernel REPL and DoE hooks
`src/starkernel/repl.c` and `src/starkernel/doe_log.c`
| Word | Stack | Description |
|---|---|---|
| `HB-ON` **K** | `( -- )` | Turns on per-tick DoE instrumentation. |
| `HB-OFF` **K** | `( -- )` | Turns off per-tick DoE instrumentation. |
| `BLK-ATTACH-ACK` **K** | `( dev-ptr ok? -- )` | The acknowledgement Artemis sends to Hera after `BLK-ATTACH`, delivered with `VM-EXEC`. On success, Hera runs the deferred Zuse or WIREBIND attach. |
| `KH-BLK-ATTACH-SEND` **K** | `( c-addr u -- ok? )` | Sends a payload to Hera through kernel-Hermes. The sender and receiver are worked out in C, so the caller cannot spoof them. A refused send is logged. |
| `KH-ELEVATE-SEND` **K** | `( c-addr u -- ok? )` | The same mechanism for elevation requests. Nothing calls it at present. |
## 34. Hera (Mama) and child-VM words
`src/starkernel/capsule/mama_forth_words.c`. These are **K** only. Hera receives every word in this section, in
both `FORTH` and the `MAMA` vocabulary. Child VMs receive only `STOP EXEC USE BIRTH CAPSULE-BIRTH VM-EXEC VM-CALL
VM-HEAT VM-ERROR? SWITCH-MARK-WORK` and the `STADIUM-*` words. VM names are matched without regard to case.
### VM lifecycle
| Word | Stack | Description |
|---|---|---|
| `BIRTH` | `( c-addr u -- )` | Births a VM from its capsule (`S" Artemis"` loads `artemis:init.4th`). If the VM is already live, nothing happens. `Hera` is rejected. |
| `KILL` | `( c-addr u -- )` | Destroys a VM. Hera cannot be killed, and killing a dead VM does nothing. |
| `START` | `( c-addr u -- )` | Enters the VM's REPL and blocks until it runs `STOP` or `BYE`. It refuses a VM that is LIVE, DEAD, or STILLBORN. |
| `STOP` | `( -- )` | Halts the current VM, so its REPL loop returns. |
| `USE` | `( c-addr u -- )` | Redirects console input to the named VM without nesting the C stack, and changes the prompt to `[Name]`. `S" Hera" USE` switches back to Hera. |
| `EXEC` | `( c-addr u -- )` | Runs a named capsule inside the current VM. |
| `EJECT` | `( -- )` | Cleanly detaches the identity attached through USB home blocks: it flushes the user VM and kills it, or logs Zuse out. |
| `CONNECT-HERMES` | `( -- )` | Enters Hermes's REPL, birthing Hermes first if needed. |
| `CONNECT-ARTEMIS` | `( -- )` | Enters Artemis's REPL, birthing Artemis first if needed. |
| `BYE` | `( -- )` | **On Hera:** reaps every child and then cold-resets the machine. |
### Cross-VM execution (compudynamics)
| Word | Stack | Description |
|---|---|---|
| `VM-EXEC` | `( cmd-a cmd-u name-a name-u -- )` | Injects a command into the named VM and runs it immediately without blocking. Example: `S" DOE-WORK" S" Hermes" VM-EXEC`. |
| `VM-CALL` | `( cmd-a cmd-u name-a name-u -- n )` | Like `VM-EXEC`, then pops the target's top of stack onto the caller's stack. If the target left nothing, it pushes 0 and sets an error. |
| `VM-STEP` | `( c-addr u -- )` | Gives the named VM one REPL turn: one prompt, one line, then it returns. |
| `VM-HEAT` | `( c-addr u -- q )` | Pushes the VM's `execution_heat_q48`. An unknown VM gives 0 and prints nothing. |
| `VM-ERROR?` | `( c-addr u -- flag )` | True if the VM has `vm->error` set. An unknown VM gives `0`. |
| `VM-COUNT` | `( -- n )` | Pushes the number of registered VMs. |
| `VM-CONSERVED?` | `( -- flag )` | True if the total fleet heat is within ε of `Q.1`. |
| `VM-PHYSICS-STATUS` | `( -- )` | Prints the fleet physics report. |
| `SWITCH-MARK-WORK` | `( c-addr u -- )` | Marks a VM as having work, which makes it eligible for a context switch. The message path calls it, and it ignores bad names silently. |
| `MAMA-VM-ID` | `( -- 0 0 )` | Pushes Hera's 128-bit VM ID, which is all zeros. |
| `NAME>XT` | `( c-addr u -- xt \| 0 )` | Looks up a name held in a data buffer. A miss gives `0`. |
### Capsules
| Word | Stack | Description |
|---|---|---|
| `CAPSULE-COUNT` | `( -- n )` | Pushes the number of entries in the capsule directory. |
| `CAPSULE@` | `( idx -- desc \| 0 )` | Pushes the descriptor for the capsule at `idx`. |
| `CAPSULE-HASH@` | `( desc -- hash )` | Pushes the capsule's content hash. |
| `CAPSULE-FLAGS@` | `( desc -- flags )` | Pushes the capsule's flags. |
| `CAPSULE-LEN@` | `( desc -- len )` | Pushes the capsule's payload length. |
| `CAPSULE-BIRTH` | `( id -- vmid-lo vmid-hi )` | Births an unnamed VM from a production (p) capsule and pushes its 128-bit ID. On failure both cells are all ones. |
| `CAPSULE-RUN` | `( id -- )` | Runs an experiment (e) capsule on Hera. |
| `CAPSULE-TEST` | `( -- )` | Prints a message confirming the capsule system is running. |
| `WORKER-BIRTH` | `( cap-a cap-u name-a name-u -- ok? )` | Births a named, `VM-EXEC`-addressable worker from any p-capsule, with no identity attached. |
| `UNATTENDED-BIRTH` | `( cap-a cap-u name-a name-u -- ok? )` | Births a VM and installs the verified identity whose `UNATTENDED-ID-UUID` and `UNATTENDED-ID-CERT` the capsule defined. |
| `CONSOLE-ATTACH` | `( name-a name-u -- ok? )` | Pairs a new console VM with the live VM registered as `<name>~user`. |
### Identity, Zuse, and diagnostics
| Word | Stack | Description |
|---|---|---|
| `MINT` | `( fn-a fn-u un-a un-u em-a em-u ph-a ph-u restrict? -- ok? )` | Mints an identity onto the attached USB drive. The full name and username are required; pass an empty string for email or phone to leave them out. A non-zero `restrict?` selects the locked-down personality that allows only FORTH-79 and FORTH-83 words. |
| `MINT-SCRATCH` | same as `MINT` | Mints onto the scratch device instead, prints the UUID, and pushes 1 or 0. |
| `MINT-SCRATCH-EMIT` | `( -- ok? )` | Prints the last scratch mint as FORTH source (`CREATE UNATTENDED-ID-UUID` / `-CERT` byte lists) for copying by hand. It refuses (pushes 0) if no mint has succeeded. |
| `ZUSE-ELIGIBILITY-ADD` | `( c-addr -- ok? )` | Adds the 32-byte Ed25519 public key at `c-addr` to Zuse's elevation list. |
| `ZUSE-ELIGIBLE?` | `( c-addr -- flag )` | Checks whether a key is on the list. It fails closed. |
| `ELEVATE-PUBKEY-UNPACK` | `( pk0 pk1 pk2 pk3 buf -- )` | Rebuilds a 32-byte public key from four cells. It is the inverse of `ZUSE-PUBKEY@`. |
| `RUNCAP-TEST` | `( c-addr u -- ok? rc )` | Diagnostic: runs `capsule_runcap_birth()` against the current home-blocks drive. |
| `PAIR-TEST` | `( c-addr u -- ok? )` | Diagnostic: births a console VM and a `<name>~user` VM as a pair. |
### Stadium (heat accounting)
Heat values are Q48.16. Each word works only on the calling VM's own quota.
| Word | Stack | Description |
|---|---|---|
| `STADIUM-ADMIT` | `( identity heat behaviour -- cell \| -1 )` | Admits a patron. `behaviour` must be 0–3. |
| `STADIUM-EVICT` | `( cell -- flag )` | Reaps the patron in `cell`. It is refused if the cell is out of range, not resident, pinned, or blocked by what it contains. |
| `STADIUM-HEAT@` | `( cell -- heat )` | Reads a resident cell's heat. A cell that is not the caller's gives 0. |
| `STADIUM-HEAT!` | `( heat cell -- )` | Writes a cell's heat, pulling the difference from the reservoir or pushing it back. An increase the reservoir cannot cover is silently refused. |
| `STADIUM-RES@` | `( -- heat )` | Pushes the VM's reservoir balance. |
| `STADIUM-RES-PULL` | `( qty -- got )` | Pulls up to `qty` from the reservoir and pushes the amount actually taken. |
| `STADIUM-RES-PUSH` | `( heat -- )` | Credits heat back to the reservoir. |
| `STADIUM-WORD-HEAT` | `( -- heat )` | Pushes the total heat held by this VM's word-execution residents. |
## 35. Hosted lifecycle stubs
`src/word_source/lifecycle_words_hosted.c`. These are **H** only; `main.c` registers them. Each word only logs
`"<WORD> <name> (hosted)"`. They exist so that capsule scripts also parse in hosted builds.
| Word | Stack | Description |
|---|---|---|
| `BIRTH` `KILL` `PAUSE` `RESUME` `USE` | `( c-addr u -- )` | No-op stubs that only write a log line. |
---
## 36. Implementation quirks to know
These behaviours differ from what a FORTH-79 or ANS programmer would expect. Each was checked against the C source.
1. **`ROLL` counts from the bottom of the stack.** With `1 2 3` on the stack, `1 ROLL` gives `2 3 1`; ANS gives
`1 3 2`. The test suite (`stack_words_test.c`) asserts the current behaviour, so it looks intended. Portable code
should use `SWAP` and `ROT`.
2. **`PICK` is 0-based,** as in ANS. FORTH-79's `PICK` was 1-based.
3. **`FIND` parses the input stream.** It does not take a counted string. Use `(FIND)` for a counted string or
`NAME>XT` (kernel only) for a name in a buffer.
4. **The Q48.16 type is unsigned.** `Q.<`, `Q.>`, `Q.MIN`, `Q.MAX` and `Q.PRINT` treat a negative Q value as a huge
positive one, and `Q.FROM-INT` turns a negative integer into 0. `Q.ABS` and `Q.NEG` do treat the top bit as a sign.
5. **`Q./` by zero returns 0 without setting an error,** while the integer `/`, `MOD`, and `*/` all set `vm->error`.
6. **`[LITERAL]` does nothing,** and `LITERAL` works only because §17 registers it again after the placeholder.
7. **`MOD`, `/MOD`, `*/`, and `*/MOD` are registered twice.** The mixed-arithmetic versions (§6) are the ones used.
8. **Four `PHYSICS-*` diagnostic words are not registered.** `PHYSICS-WORD-METRICS`, `PHYSICS-CALC-KNOBS`,
`PHYSICS-BURN` and `PHYSICS-SHOW-FEEDBACK` are defined in C but never added to the dictionary.
9. **`does_rt` is a visible dictionary entry.** It is an internal helper; do not call it.
10. **The `STARFORTH` vocabulary registers its words twice** (once in `FORTH`, once in `STARFORTH`). Hera does the
same with `MAMA`. As a result, those names appear twice in `WORDS` output.
+12
View File
@@ -385,6 +385,18 @@ int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn);
*/
int blk_get_device_range(struct blkio_dev *dev, uint32_t *out_start_lbn, uint32_t *out_count);
/* blk_lbn_device_handle - the missing direction from blk_get_device_range()
* above (that goes device -> LBN range; this goes LBN -> device). Returns
* an opaque, stable handle -- directly comparable with == to test "do
* these two LBNs belong to the same physical device" -- or NULL if lbn
* doesn't resolve to any attached device. Never dereference the returned
* pointer; it exists only for equality comparison (Phase 8 v3, 2026-09-23:
* added so MOVE/CMOVE/CMOVE> can refuse a same-VM, cross-device raw block
* copy through the block-window mechanism -- see those words' own callers
* in memory_words.c/string_words.c).
*/
const void *blk_lbn_device_handle(uint32_t lbn);
/* blk_get_device_free_blocks - free vs. total 1 KiB FORTH-block count for
* a specific already-attached device (same slot lookup as
* blk_get_device_range()). RAM-backed slots (no on-disk vol_meta) report
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16
View File
@@ -805,6 +805,18 @@ int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn) {
if (!blk_is_valid(home_lbn) || !blk_is_valid(target_lbn)) return BLK_ERANGE;
/* Same-device only (Phase 8 v3, 2026-09-23): this function's own purpose
* is relocating a block to a new LBN on the device it's being relocated
* to (wear-leveling) -- never documented as cross-device, and nothing
* enforced that until now. Without this check, RELOCATE-BLOCK is a
* ready-made, unpinned, silent cross-device raw block copy primitive --
* closing that is the whole point of this change. */
if (lbn_to_slot(resolve_lbn(home_lbn)) != lbn_to_slot(resolve_lbn(target_lbn))) {
log_message(LOG_WARN,
"blk: refused cross-device relocate LBN %u -> %u", home_lbn, target_lbn);
return BLK_EINVAL;
}
/* Stage through a local buffer rather than copying directly from one
* blk_get_buffer() result to another -- obtaining the target buffer
* can trigger a cache eviction (cache_get_slot()'s FIFO shift) that
@@ -1194,6 +1206,10 @@ int blk_get_device_range(struct blkio_dev *dev, uint32_t *out_start_lbn, uint32_
return BLK_OK;
}
const void *blk_lbn_device_handle(uint32_t lbn) {
return (const void *) lbn_to_slot(resolve_lbn(lbn));
}
int blk_get_device_free_blocks(struct blkio_dev *dev, uint64_t *out_free, uint64_t *out_total) {
if (!dev || !out_free || !out_total) return BLK_EINVAL;
blk_dev_slot_t *slot = slot_by_dev(dev);
+40 -1
View File
@@ -174,7 +174,7 @@ void empty_all_buffers(VM *vm) {
* the top of every function below that reads vm->blk_vm_lbn[]/
* vm->blk_vm_cbuf[] directly, not just blk_vm_find() -- blk_vm_flush_all()
* walks the same arrays without going through blk_vm_find() first. */
static void blk_vm_check_epoch(VM *vm) {
void blk_vm_check_epoch(VM *vm) {
uint64_t epoch = blk_subsys_epoch();
if (epoch == vm->blk_vm_epoch) return;
for (int i = 0; i < BLK_VM_SLOTS; i++) {
@@ -185,6 +185,45 @@ static void blk_vm_check_epoch(VM *vm) {
vm->blk_vm_epoch = epoch;
}
/* blk_vm_slot_for_addr ( addr -- slot-index|-1 ): does `addr` fall inside
* the block-window range at all? Pure address arithmetic, no VM state --
* the window's base/size are fixed compile-time constants (vm.h), so this
* doesn't need a VM* the way blk_vm_find() does (that also needs the
* per-VM blk_vm_lbn[]/cbuf[] arrays to check what's actually loaded).
* Exposed (Phase 8 v3, 2026-09-23) for MOVE/CMOVE/CMOVE>'s cross-device
* block-copy check -- they need "is this a block-window address" without
* needing to know or care whether anything's actually loaded there. */
int blk_vm_slot_for_addr(vaddr_t addr) {
if (addr < BLK_VM_WINDOW_BASE || addr >= BLK_VM_WINDOW_BASE + BLK_VM_WINDOW_SIZE)
return -1;
return (int) ((addr - BLK_VM_WINDOW_BASE) / BLOCK_SIZE);
}
/* blk_vm_is_cross_device_copy ( addr1 addr2 -- refuse? ): the actual
* Phase 8 v3 (2026-09-23) check MOVE/CMOVE/CMOVE> call before copying.
* True only when BOTH addresses are block-window addresses AND the LBNs
* currently loaded in those two slots resolve to two different physical
* devices -- the exact shape of `<src> BLOCK <dst> BUFFER ... MOVE`, a
* same-VM, cross-device raw block copy using only stock words. A copy
* where either (or both) address is ordinary VM memory -- the overwhelming
* common case, e.g. staging PAD text into a block, or reading a block out
* for display -- is never flagged: only both-window, both-different-device
* copies are refused. Callers must still do their own vm_addr_ok() bounds
* checks first; this only adds the device-boundary refusal on top. */
int blk_vm_is_cross_device_copy(VM *vm, vaddr_t addr1, vaddr_t addr2) {
int slot1 = blk_vm_slot_for_addr(addr1);
int slot2 = blk_vm_slot_for_addr(addr2);
if (slot1 < 0 || slot2 < 0) return 0;
blk_vm_check_epoch(vm);
uint32_t lbn1 = vm->blk_vm_lbn[slot1];
uint32_t lbn2 = vm->blk_vm_lbn[slot2];
if (lbn1 == 0 || lbn2 == 0) return 0; /* an empty/unassigned slot has nothing to protect */
const void *dev1 = blk_lbn_device_handle(lbn1);
const void *dev2 = blk_lbn_device_handle(lbn2);
if (!dev1 || !dev2) return 0; /* can't resolve -- not this check's job to refuse */
return dev1 != dev2;
}
/* Find the slot holding lbn; return slot index or -1 if not loaded. */
static int blk_vm_find(VM *vm, uint32_t lbn) {
blk_vm_check_epoch(vm);
+35
View File
@@ -158,4 +158,39 @@ void blk_vm_flush_all(VM * vm);
*/
void empty_all_buffers(VM * vm);
/**
* @brief Discards vm->blk_vm_lbn[]/cbuf[]/dirty[] if the device chain has
* changed since last validated (see this function's own doc comment in
* block_words.c for why). Exposed (Phase 8 v3, 2026-09-23) so callers
* outside this file that read vm->blk_vm_lbn[] directly (MOVE/CMOVE's
* cross-device block-copy check, memory_words.c/string_words.c) go through
* the same invalidation contract blk_vm_find() already uses internally,
* rather than a second, divergent copy of this logic.
* @param vm Pointer to the Forth virtual machine instance
*/
void blk_vm_check_epoch(VM *vm);
/**
* @brief Does `addr` fall inside the block I/O window? Pure address
* arithmetic against the window's fixed compile-time base/size -- no VM
* state needed. Exposed (Phase 8 v3, 2026-09-23) for MOVE/CMOVE/CMOVE>'s
* cross-device block-copy check.
* @param addr VM offset to test
* @return slot index (0..BLK_VM_SLOTS-1) if inside the window, -1 otherwise
*/
int blk_vm_slot_for_addr(vaddr_t addr);
/**
* @brief True if addr1/addr2 are both block-window addresses currently
* loaded from two different physical devices (Phase 8 v3, 2026-09-23) --
* the check MOVE/CMOVE/CMOVE> call before copying to refuse a same-VM,
* cross-device raw block copy. See this function's own doc comment in
* block_words.c for exactly what is and isn't flagged.
* @param vm VM performing the copy
* @param addr1 first VM offset (source or destination, order doesn't matter)
* @param addr2 second VM offset
* @return 1 if this copy should be refused, 0 otherwise
*/
int blk_vm_is_cross_device_copy(VM *vm, vaddr_t addr1, vaddr_t addr2);
#endif /* BLOCK_WORDS_H */
+7
View File
@@ -44,6 +44,7 @@
#include "../../include/word_registry.h"
#include "vm.h"
#include "../../include/log.h"
#include "include/block_words.h" /* blk_vm_is_cross_device_copy() -- Phase 8 v3 */
#include <string.h>
@@ -231,6 +232,12 @@ void memory_word_move(VM *vm) {
vm->error = 1;
return;
}
/* Phase 8 v3 (2026-09-23): refuse a same-VM, cross-device raw block
* copy -- see blk_vm_is_cross_device_copy()'s own doc comment. */
if (blk_vm_is_cross_device_copy(vm, addr1, addr2)) {
vm->error = 1;
return;
}
uint8_t *src = vm_ptr(vm, addr1);
uint8_t *dst = vm_ptr(vm, addr2);
memmove(dst, src, len);
+17
View File
@@ -46,6 +46,7 @@
#include "../../include/log.h"
#include "../../include/vm.h"
#include "../../include/vm_api.h"
#include "include/block_words.h" /* blk_vm_is_cross_device_copy() -- Phase 8 v3 */
#include <stdio.h>
#include <string.h>
#include <ctype.h>
@@ -626,6 +627,14 @@ static void string_word_cmove(VM *vm) {
(long) addr1, (long) addr2, (long) u);
return;
}
/* Phase 8 v3 (2026-09-23): refuse a same-VM, cross-device raw block
* copy -- see blk_vm_is_cross_device_copy()'s own doc comment. */
if (blk_vm_is_cross_device_copy(vm, src, dst)) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE: refused cross-device block copy (src=%ld dst=%ld)",
(long) addr1, (long) addr2);
return;
}
/* Ascending copy (low->high) */
for (size_t i = 0; i < n; i++) {
@@ -665,6 +674,14 @@ static void string_word_cmove_greater(VM *vm) {
(long) addr1, (long) addr2, (long) u);
return;
}
/* Phase 8 v3 (2026-09-23): refuse a same-VM, cross-device raw block
* copy -- see blk_vm_is_cross_device_copy()'s own doc comment. */
if (blk_vm_is_cross_device_copy(vm, src, dst)) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE>: refused cross-device block copy (src=%ld dst=%ld)",
(long) addr1, (long) addr2);
return;
}
/* Descending copy (high->low) */
for (size_t i = n; i > 0; i--) {