Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+58
View File
@@ -0,0 +1,58 @@
# src/
Hosted StarForth VM implementation (compiled by the root `Makefile`).
Bare-metal kernel sources live in `src/starkernel/`; FORTH word
implementations in `src/word_source/`; the test harness in
`src/test_runner/`; platform shims in `src/platform/`.
**Entry point / interpreter core**
- `main.c` — CLI entry point, VM init, DoE mode dispatch.
- `vm.c` — interpreter loop, stacks, dictionary state (the central
runtime file).
- `vm_api.c` — external VM API implementation.
- `vm_bootstrap.c` — VM bootstrap initialization.
- `vm_debug.c` — debugging utilities.
- `vm_time.c` — time-related VM operations.
- `vm_internal.h` — internal-only declarations shared across the `vm_*.c`
files, not part of the public `include/vm_api.h` surface.
- `repl.c` — REPL read-eval-print loop.
- `cli.c` — CLI argument parsing.
- `io.c` — I/O operations.
- `log.c` — logging infrastructure.
**Memory / dictionary / blocks**
- `memory_management.c` — dictionary allocator.
- `dictionary_management.c` — dictionary allocation and search.
- `dictionary_heat_optimization.c` — Loop #1 execution-heat tracking.
- `word_registry.c` — word registration system.
- `block_subsystem.c` — logical→physical block mapper.
- `blkio_file.c`, `blkio_ram.c`, `blkio_factory.c` — block I/O backends
(file-backed, RAM-backed) and the factory that selects between them.
- `stack_management.c` — stack operations.
**Physics-driven adaptive runtime (7 feedback loops)**
- `physics_runtime.c` — main physics coordinator.
- `physics_hotwords_cache.c` — Loop #1 hot-words caching.
- `physics_metadata.c` — per-word metadata tracking.
- `physics_pipelining_metrics.c` — Loop #4 word-transition prediction.
- `physics_execution_hooks.c` — execution instrumentation.
- `rolling_window_of_truth.c` — Loop #2 circular execution-history buffer.
- `inference_engine.c` — Loops #5/#6, statistical inference (window-width,
decay-slope).
- `ssm_jacquard.c` — L8 Jacquard steady-state mode selector; consumes
`compudynamics.c` for tuning-word/config lookups.
- `compudynamics.c` — generic compudynamics module (`cd_tuning_word()`,
`cd_tuning_vm()`); the score/UCB/reward/weight constants for the L8
adaptive table live here, not in `ssm_jacquard.c`.
- `heartbeat_export.c` — heartbeat metrics export (CSV export function
itself not yet implemented — see `docs/working/architecture/heartbeat_csv_export.md`).
**Math / measurement**
- `math_portable.c` — portable math functions.
- `profiler.c` — performance profiling.
- `doe_metrics.c` — Design of Experiments metrics (2^7 factorial).
Any `.bak` file alongside a `.c` file here (`doe_metrics.c.bak`,
`inference_engine.c.bak`, `vm.c.bak`) is a pre-edit backup left by a past
maintenance script (see `scripts/remove_loop_conditionals.sh`), not a
build input.
+170
View File
@@ -0,0 +1,170 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
// -----------------------------------------------------------------------------
// StarForth - Block I/O Factory (Implementation)
// -----------------------------------------------------------------------------
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "blkio.h"
#include "blkio_factory.h"
/* Backends */
extern size_t blkio_file_state_size(void);
extern const blkio_vtable_t *blkio_file_vtable(void);
extern int blkio_file_init_state(void *state_mem, size_t state_len,
const char *path,
uint32_t total_blocks,
uint32_t fbs,
uint8_t read_only,
uint8_t create_if_missing,
uint8_t truncate_to_size,
void **out_opaque);
extern size_t blkio_ram_state_size(void);
extern const blkio_vtable_t *blkio_ram_vtable(void);
extern int blkio_ram_init_state(void *state_mem, size_t state_len,
uint8_t *base, uint32_t total_blocks,
uint32_t fbs, uint8_t read_only,
void **out_opaque);
/** @brief Return 1 if string @c s is non-NULL and non-empty. */
static int nonempty(const char *s) { return (s && *s); }
/**
* @brief Open a block I/O device using the best available backend.
*
* Selects the file backend when @c disk_img_path is non-empty, otherwise
* falls back to the RAM backend using @c ram_base / @c ram_blocks.
*
* File backend behaviour:
* - Creates the file if it does not exist (unless @c read_only is set).
* - Derives @c total_blocks from the file size when @c total_blocks == 0.
* - Sets @c *out_used_file = 1 on success.
*
* RAM backend behaviour:
* - @c ram_base must point to at least @c ram_blocks * @c fbs bytes of
* caller-owned memory that outlives the device.
*
* @param dev Output device handle to populate
* @param disk_img_path Path to disk image file, or NULL/empty for RAM
* @param read_only 1 = open read-only, 0 = read-write
* @param total_blocks Desired block count (0 = derive from file size)
* @param fbs FORTH block size in bytes (0 = use @c BLKIO_FORTH_BLOCK_SIZE)
* @param state_mem Caller-allocated state buffer (must be large enough)
* @param state_len Size of @c state_mem in bytes
* @param ram_base Base of RAM block array (used only when no disk path)
* @param ram_blocks Number of blocks in @c ram_base
* @param out_used_file Optional output: set to 1 if the file backend was used
* @return @c BLKIO_OK on success, or a @c BLKIO_E* error code
*/
int blkio_factory_open(blkio_dev_t *dev,
const char *disk_img_path,
uint8_t read_only,
uint32_t total_blocks,
uint32_t fbs,
void *state_mem,
size_t state_len,
uint8_t *ram_base,
uint32_t ram_blocks,
uint8_t *out_used_file) {
if (out_used_file) *out_used_file = 0;
if (!dev || !state_mem || state_len == 0) return BLKIO_EINVAL;
const uint32_t efbs = (fbs ? fbs : BLKIO_FORTH_BLOCK_SIZE);
if (nonempty(disk_img_path)) {
const size_t need = blkio_file_state_size();
if (state_len < need) return BLKIO_ENOSPACE;
void *opaque = NULL;
const uint8_t create_if_missing = read_only ? 0u : 1u;
const uint8_t truncate_to_size = (!read_only && total_blocks > 0) ? 1u : 0u;
int rc = blkio_file_init_state(state_mem, state_len,
disk_img_path,
total_blocks,
efbs,
read_only,
create_if_missing,
truncate_to_size,
&opaque);
if (rc != BLKIO_OK) return rc;
blkio_params_t p = {
.forth_block_size = efbs,
.total_blocks = total_blocks, /* 0 => derive in file_open */
.opaque = opaque
};
rc = blkio_open(dev, blkio_file_vtable(), &p);
if (rc == BLKIO_OK && out_used_file) *out_used_file = 1u;
return rc;
}
if (!ram_base || ram_blocks == 0) return BLKIO_EINVAL;
const size_t need_ram = blkio_ram_state_size();
if (state_len < need_ram) return BLKIO_ENOSPACE;
void *opaque = NULL;
int rc = blkio_ram_init_state(state_mem, state_len,
ram_base, ram_blocks,
efbs, read_only,
&opaque);
if (rc != BLKIO_OK) return rc;
const blkio_params_t p = {
.forth_block_size = efbs,
.total_blocks = ram_blocks,
.opaque = opaque
};
return blkio_open(dev, blkio_ram_vtable(), &p);
}
+400
View File
@@ -0,0 +1,400 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
// -----------------------------------------------------------------------------
// StarForth - File-backed Block I/O backend (portable)
// - Uses lseek()+read()/write() shims (no pread/pwrite requirement).
// - Deterministic full-block I/O; rejects short I/O.
// - Single-process/serialized access expected for correctness.
// -----------------------------------------------------------------------------
#include "blkio.h"
#include <unistd.h> // read, write, lseek, close
#include <fcntl.h> // open flags
#include <sys/stat.h> // fstat
#include <sys/types.h> // off_t
#include <errno.h>
#include <string.h> // memset
#include <stdint.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
typedef struct {
int fd;
char path[PATH_MAX];
uint32_t total_blocks;
uint32_t fbs;
uint8_t read_only;
} blkio_file_state_t;
/**
* @brief Return the byte size required for a @c blkio_file_state_t.
*
* Used by @c blkio_factory_open() to verify the caller's state buffer is
* large enough before calling @c blkio_file_init_state().
*
* @return sizeof(blkio_file_state_t)
*/
size_t blkio_file_state_size(void) { return sizeof(blkio_file_state_t); }
/**
* @brief Initialise a @c blkio_file_state_t in caller-provided memory.
*
* Copies the path into state (truncated to PATH_MAX-1), sets @c fbs,
* @c total_blocks, and @c read_only, and writes the state pointer to
* @c *out_opaque. The file descriptor is initialised to -1; the actual
* @c open() call happens in @c file_open() (the vtable open method).
* @c create_if_missing and @c truncate_to_size are accepted for API
* compatibility but currently unused (creation is handled in @c file_open()).
*
* @param state_mem Caller-allocated state buffer
* @param state_len Size of @c state_mem (must be >= blkio_file_state_size())
* @param path Path to the disk image file
* @param total_blocks Total block count (0 = derive from file size at open)
* @param fbs FORTH block size in bytes
* @param read_only 1 = open read-only, 0 = read-write
* @param create_if_missing Ignored (reserved for future use)
* @param truncate_to_size Ignored (reserved for future use)
* @param out_opaque Output: set to @c state_mem on success
* @return @c BLKIO_OK on success, @c BLKIO_EINVAL on bad arguments
*/
int blkio_file_init_state(void *state_mem, size_t state_len,
const char *path,
uint32_t total_blocks,
uint32_t fbs,
uint8_t read_only,
uint8_t create_if_missing,
uint8_t truncate_to_size,
void **out_opaque) {
(void) create_if_missing;
(void) truncate_to_size;
if (!state_mem || state_len < sizeof(blkio_file_state_t) || !path || !out_opaque)
return BLKIO_EINVAL;
blkio_file_state_t *st = (blkio_file_state_t *) state_mem;
memset(st, 0, sizeof(*st));
st->fd = -1;
size_t n = 0;
while (path[n] && n < (PATH_MAX - 1)) {
st->path[n] = path[n];
n++;
}
st->path[n] = '\0';
if (st->path[0] == '\0') return BLKIO_EINVAL;
st->fbs = (fbs ? fbs : BLKIO_FORTH_BLOCK_SIZE);
st->total_blocks = total_blocks; // may be 0 (derive later)
st->read_only = read_only ? 1u : 0u;
*out_opaque = (void *) st;
return BLKIO_OK;
}
/**
* @brief Perform a complete read of @c len bytes at absolute offset @c off_abs.
*
* Uses @c lseek() + @c read() loops to handle short reads and EINTR.
* Not safe for concurrent readers/writers on the same file descriptor.
* Returns -1 on any I/O error or unexpected EOF.
*
* @param fd Open file descriptor
* @param buf Destination buffer (must be >= @c len bytes)
* @param len Number of bytes to read
* @param off_abs Absolute byte offset within the file
* @return 0 on success, -1 on error (errno set)
*/
static int full_read_at(int fd, void *buf, size_t len, off_t off_abs) {
uint8_t *p = (uint8_t *) buf;
size_t done = 0;
while (done < len) {
if (lseek(fd, off_abs + (off_t) done, SEEK_SET) == (off_t) - 1) return -1;
ssize_t r = read(fd, p + done, len - done);
if (r < 0) {
if (errno == EINTR) continue;
return -1;
}
if (r == 0) {
errno = EIO;
return -1;
} /* unexpected EOF */
done += (size_t) r;
}
return 0;
}
/**
* @brief Perform a complete write of @c len bytes at absolute offset @c off_abs.
*
* Uses @c lseek() + @c write() loops to handle short writes and EINTR.
* Not safe for concurrent readers/writers on the same file descriptor.
* Returns -1 on any I/O error.
*
* @param fd Open file descriptor
* @param buf Source buffer (must be >= @c len bytes)
* @param len Number of bytes to write
* @param off_abs Absolute byte offset within the file
* @return 0 on success, -1 on error (errno set)
*/
static int full_write_at(int fd, const void *buf, size_t len, off_t off_abs) {
const uint8_t *p = (const uint8_t *) buf;
size_t done = 0;
while (done < len) {
if (lseek(fd, off_abs + (off_t) done, SEEK_SET) == (off_t) - 1) return -1;
ssize_t w = write(fd, p + done, len - done);
if (w < 0) {
if (errno == EINTR) continue;
return -1;
}
if (w == 0) {
errno = EIO;
return -1;
} /* should not happen */
done += (size_t) w;
}
return 0;
}
/** @brief Open the file and attach it to the device handle. */
static int file_open(blkio_dev_t *dev, const blkio_params_t *p);
/** @brief Close the file descriptor held by the device. */
static int file_close(blkio_dev_t * dev);
/** @brief Read one FORTH block from the file into @c dst. */
static int file_read(blkio_dev_t *dev, uint32_t fblock, void *dst);
/** @brief Write one FORTH block from @c src into the file. */
static int file_write(blkio_dev_t *dev, uint32_t fblock, const void *src);
/** @brief Flush (fsync) pending writes to the file. */
static int file_flush(blkio_dev_t * dev);
/** @brief Return device info (block size, count, physical size, read-only flag). */
static int file_info(blkio_dev_t * dev, blkio_info_t * out);
static const blkio_vtable_t BLKIO_FILE_VT = {
.open = file_open,
.close = file_close,
.read = file_read,
.write = file_write,
.flush = file_flush,
.info = file_info
};
/**
* @brief Return the vtable for the file-backed block I/O backend.
* @return Pointer to the static @c blkio_vtable_t for file devices
*/
const blkio_vtable_t *blkio_file_vtable(void) { return &BLKIO_FILE_VT; }
/**
* @brief Open the underlying file and populate the device handle.
*
* Opens @c st->path with @c O_RDONLY or @c O_RDWR|O_CREAT depending on
* @c st->read_only. Derives @c total_blocks from the file size if the state
* value is 0 — the file size must then be a multiple of @c fbs. Sets
* @c dev->state, @c dev->forth_block_size, and @c dev->total_blocks on success.
*
* @param dev Uninitialised device handle
* @param p Parameters including the pre-initialised opaque state pointer
* @return @c BLKIO_OK on success, @c BLKIO_EIO or @c BLKIO_EINVAL on failure
*/
static int file_open(blkio_dev_t *dev, const blkio_params_t *p) {
if (!dev || !p || !p->opaque) return BLKIO_EINVAL;
blkio_file_state_t *st = (blkio_file_state_t *) p->opaque;
if (st->fd != -1) {
close(st->fd);
st->fd = -1;
}
int oflags = st->read_only ? O_RDONLY : O_RDWR;
if (!st->read_only) oflags |= O_CREAT;
#ifdef O_CLOEXEC
oflags |= O_CLOEXEC;
#endif
const int fd = open(st->path, oflags, 0644);
if (fd < 0) return BLKIO_EIO;
st->fd = fd;
struct stat sb;
if (fstat(fd, &sb) < 0) {
close(fd);
st->fd = -1;
return BLKIO_EIO;
}
/* If caller didn’t specify blocks, derive from file size (must be multiple of fbs). */
if (st->total_blocks == 0) {
if (st->fbs == 0) {
close(fd);
st->fd = -1;
return BLKIO_EINVAL;
}
if ((uint64_t) sb.st_size % (uint64_t) st->fbs != 0) {
close(fd);
st->fd = -1;
return BLKIO_EINVAL;
}
uint64_t blocks = (uint64_t) sb.st_size / (uint64_t) st->fbs;
if (blocks == 0 || blocks > 0xffffffffULL) {
close(fd);
st->fd = -1;
return BLKIO_EINVAL;
}
st->total_blocks = (uint32_t) blocks;
}
dev->state = st;
dev->forth_block_size = st->fbs;
dev->total_blocks = st->total_blocks;
return BLKIO_OK;
}
/**
* @brief Close the file descriptor held by the device.
*
* @param dev Device handle (must have valid state)
* @return @c BLKIO_OK on success, @c BLKIO_EIO if @c close() fails
*/
static int file_close(blkio_dev_t *dev) {
if (!dev || !dev->state) return BLKIO_EINVAL;
blkio_file_state_t *st = (blkio_file_state_t *) dev->state;
if (st->fd >= 0) {
int rc = close(st->fd);
st->fd = -1;
return (rc == 0) ? BLKIO_OK : BLKIO_EIO;
}
return BLKIO_OK;
}
/**
* @brief Read one FORTH block from the file backend.
*
* @param dev Open file device
* @param fblock Logical block number (must be < @c dev->total_blocks)
* @param dst Destination buffer (must be >= @c fbs bytes)
* @return @c BLKIO_OK, @c BLKIO_EINVAL if @c fblock is out of range, or @c BLKIO_EIO
*/
static int file_read(blkio_dev_t *dev, uint32_t fblock, void *dst) {
if (!dev || !dst) return BLKIO_EINVAL;
if (fblock >= dev->total_blocks) return BLKIO_EINVAL;
blkio_file_state_t *st = (blkio_file_state_t *) dev->state;
if (st->fd < 0) return BLKIO_ECLOSED;
const off_t off = (off_t)((uint64_t) fblock * (uint64_t) st->fbs);
if (full_read_at(st->fd, dst, st->fbs, off) < 0) return BLKIO_EIO;
return BLKIO_OK;
}
/**
* @brief Write one FORTH block to the file backend.
*
* @param dev Open file device (must not be read-only)
* @param fblock Logical block number (must be < @c dev->total_blocks)
* @param src Source buffer (must be >= @c fbs bytes)
* @return @c BLKIO_OK, @c BLKIO_ENOSUP if read-only, @c BLKIO_EINVAL if out of range,
* or @c BLKIO_EIO on write failure
*/
static int file_write(blkio_dev_t *dev, uint32_t fblock, const void *src) {
if (!dev || !src) return BLKIO_EINVAL;
if (fblock >= dev->total_blocks) return BLKIO_EINVAL;
blkio_file_state_t *st = (blkio_file_state_t *) dev->state;
if (st->fd < 0) return BLKIO_ECLOSED;
if (st->read_only) return BLKIO_ENOSUP;
const off_t off = (off_t)((uint64_t) fblock * (uint64_t) st->fbs);
if (full_write_at(st->fd, src, st->fbs, off) < 0) return BLKIO_EIO;
return BLKIO_OK;
}
/**
* @brief Flush pending writes via @c fsync() on Linux/POSIX; no-op elsewhere.
*
* @param dev Open file device
* @return @c BLKIO_OK on success, @c BLKIO_EIO if @c fsync() fails
*/
static int file_flush(blkio_dev_t *dev) {
if (!dev) return BLKIO_EINVAL;
blkio_file_state_t *st = (blkio_file_state_t *) dev->state;
#if defined(__linux__) || defined(__APPLE__) || defined(__unix__)
if (st->fd < 0) return BLKIO_ECLOSED;
if (fsync(st->fd) < 0) return BLKIO_EIO;
return BLKIO_OK;
#else
(void) st;
return BLKIO_OK;
#endif
}
/**
* @brief Return device info for the file backend.
*
* Physical sector size is reported as 512 (best-effort guess; no ioctl used).
* Physical size in bytes comes from @c fstat().
*
* @param dev Open file device
* @param out Output struct to populate
* @return @c BLKIO_OK on success, @c BLKIO_EIO if @c fstat() fails
*/
static int file_info(blkio_dev_t *dev, blkio_info_t *out) {
if (!dev || !out) return BLKIO_EINVAL;
blkio_file_state_t *st = (blkio_file_state_t *) dev->state;
struct stat sb;
if (fstat(st->fd, &sb) < 0) return BLKIO_EIO;
out->forth_block_size = st->fbs;
out->total_blocks = st->total_blocks;
out->phys_sector_size = 512; // best-effort guess
out->phys_size_bytes = (uint64_t) sb.st_size;
out->read_only = st->read_only;
return BLKIO_OK;
}
+237
View File
@@ -0,0 +1,237 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
// -----------------------------------------------------------------------------
// StarForth - RAM Block I/O backend
// -----------------------------------------------------------------------------
#include "blkio.h"
#include <string.h>
typedef struct {
uint8_t *base;
uint32_t total_blocks;
uint32_t fbs;
uint8_t read_only;
} blkio_ram_state_t;
/**
* @brief Return the byte size required for a @c blkio_ram_state_t.
* @return sizeof(blkio_ram_state_t)
*/
size_t blkio_ram_state_size(void) { return sizeof(blkio_ram_state_t); }
/**
* @brief Initialise a @c blkio_ram_state_t in caller-provided memory.
*
* Stores @c base, @c total_blocks, @c fbs (defaulting to
* @c BLKIO_FORTH_BLOCK_SIZE if zero), and @c read_only into the state struct
* at @c state_mem. Sets @c *out_opaque to point to the state. The caller
* owns the @c base memory and must ensure it remains valid for the device lifetime.
*
* @param state_mem Caller-allocated buffer (must be >= blkio_ram_state_size())
* @param state_len Size of @c state_mem in bytes
* @param base Pointer to the RAM block array
* @param total_blocks Number of FORTH blocks in @c base
* @param fbs FORTH block size in bytes (0 = use default)
* @param read_only 1 = deny writes, 0 = allow reads and writes
* @param out_opaque Output: set to @c state_mem on success
* @return @c BLKIO_OK on success, @c BLKIO_EINVAL on bad arguments
*/
int blkio_ram_init_state(void *state_mem, size_t state_len,
uint8_t *base, uint32_t total_blocks,
uint32_t fbs, uint8_t read_only,
void **out_opaque) {
if (!state_mem || state_len < sizeof(blkio_ram_state_t) || !base || !out_opaque)
return BLKIO_EINVAL;
blkio_ram_state_t *st = (blkio_ram_state_t *) state_mem;
st->base = base;
st->total_blocks = total_blocks;
st->fbs = (fbs ? fbs : BLKIO_FORTH_BLOCK_SIZE);
st->read_only = read_only ? 1u : 0u;
*out_opaque = (void *) st;
return BLKIO_OK;
}
/** @brief Attach state to the device handle and validate parameters. */
static int ram_open(blkio_dev_t *dev, const blkio_params_t *p);
/** @brief Close the RAM device (no-op; caller owns the memory). */
static int ram_close(blkio_dev_t * dev);
/** @brief Read one FORTH block from the RAM array into @c dst. */
static int ram_read(blkio_dev_t *dev, uint32_t fblock, void *dst);
/** @brief Write one FORTH block from @c src into the RAM array. */
static int ram_write(blkio_dev_t *dev, uint32_t fblock, const void *src);
/** @brief Flush the RAM device — always a no-op since data is in memory. */
static int ram_flush(blkio_dev_t * dev);
/** @brief Return device info (block size, count, physical size, read-only flag). */
static int ram_info(blkio_dev_t * dev, blkio_info_t * out);
static const blkio_vtable_t BLKIO_RAM_VT = {
.open = ram_open,
.close = ram_close,
.read = ram_read,
.write = ram_write,
.flush = ram_flush,
.info = ram_info
};
/**
* @brief Return the vtable for the RAM block I/O backend.
* @return Pointer to the static @c blkio_vtable_t for RAM devices
*/
const blkio_vtable_t *blkio_ram_vtable(void) { return &BLKIO_RAM_VT; }
/**
* @brief Test whether @c fb is a valid block number for the RAM device.
* @param st RAM state (may be NULL)
* @param fb Block number to test
* @return 1 if in bounds, 0 otherwise
*/
static inline int in_bounds(const blkio_ram_state_t *st, uint32_t fb) {
return st && (fb < st->total_blocks);
}
/**
* @brief Attach the RAM state to the device handle.
*
* Validates that @c base and @c fbs are non-zero, then sets
* @c dev->state, @c dev->forth_block_size, and @c dev->total_blocks.
*
* @param dev Uninitialised device handle
* @param p Parameters with pre-initialised opaque RAM state pointer
* @return @c BLKIO_OK on success, @c BLKIO_EINVAL on bad arguments
*/
static int ram_open(blkio_dev_t *dev, const blkio_params_t *p) {
if (!dev || !p || !p->opaque) return BLKIO_EINVAL;
blkio_ram_state_t *st = (blkio_ram_state_t *) p->opaque;
if (!st->base || st->fbs == 0) return BLKIO_EINVAL;
dev->state = st;
dev->forth_block_size = st->fbs;
dev->total_blocks = st->total_blocks;
return BLKIO_OK;
}
/**
* @brief Close the RAM device — caller retains ownership of the RAM buffer.
*
* @param dev Device handle
* @return @c BLKIO_OK always (no resources to release)
*/
static int ram_close(blkio_dev_t *dev) {
if (!dev || !dev->state) return BLKIO_EINVAL;
return BLKIO_OK; /* caller owns memory */
}
/**
* @brief Copy one FORTH block from the RAM array to @c dst.
*
* @param dev Open RAM device
* @param fblock Logical block number (must be < @c total_blocks)
* @param dst Destination buffer (must be >= @c fbs bytes)
* @return @c BLKIO_OK on success, @c BLKIO_EINVAL if out of range or NULL pointer
*/
static int ram_read(blkio_dev_t *dev, uint32_t fblock, void *dst) {
if (!dev || !dst) return BLKIO_EINVAL;
blkio_ram_state_t *st = (blkio_ram_state_t *) dev->state;
if (!in_bounds(st, fblock)) return BLKIO_EINVAL;
const uint8_t *src = st->base + ((size_t) fblock * st->fbs);
memcpy(dst, src, st->fbs);
return BLKIO_OK;
}
/**
* @brief Copy one FORTH block from @c src into the RAM array.
*
* @param dev Open RAM device (must not be read-only)
* @param fblock Logical block number (must be < @c total_blocks)
* @param src Source buffer (must be >= @c fbs bytes)
* @return @c BLKIO_OK on success, @c BLKIO_ENOSUP if read-only,
* @c BLKIO_EINVAL if out of range or NULL pointer
*/
static int ram_write(blkio_dev_t *dev, uint32_t fblock, const void *src) {
if (!dev || !src) return BLKIO_EINVAL;
blkio_ram_state_t *st = (blkio_ram_state_t *) dev->state;
if (st->read_only) return BLKIO_ENOSUP;
if (!in_bounds(st, fblock)) return BLKIO_EINVAL;
uint8_t *dst = st->base + ((size_t) fblock * st->fbs);
memcpy(dst, src, st->fbs);
return BLKIO_OK;
}
/**
* @brief Flush the RAM device — no-op since data is always in memory.
*
* @param dev Device handle (unused)
* @return @c BLKIO_OK always
*/
static int ram_flush(blkio_dev_t *dev) {
(void) dev;
return BLKIO_OK;
}
/**
* @brief Return device info for the RAM backend.
*
* Physical sector size equals @c fbs (RAM has no physical sector constraint).
* Physical size in bytes = @c fbs * @c total_blocks.
*
* @param dev Open RAM device
* @param out Output struct to populate
* @return @c BLKIO_OK on success, @c BLKIO_EINVAL if @c dev or @c out is NULL
*/
static int ram_info(blkio_dev_t *dev, blkio_info_t *out) {
if (!dev || !out) return BLKIO_EINVAL;
blkio_ram_state_t *st = (blkio_ram_state_t *) dev->state;
out->forth_block_size = st->fbs;
out->total_blocks = st->total_blocks;
out->phys_sector_size = st->fbs;
out->phys_size_bytes = (uint64_t) st->fbs * (uint64_t) st->total_blocks;
out->read_only = st->read_only;
return BLKIO_OK;
}
File diff suppressed because it is too large Load Diff
+238
View File
@@ -0,0 +1,238 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cli.h"
#include "version.h"
/* Parse helper for --ram-disk */
static int parse_u32(const char* s, uint32_t* out)
{
if (!s || !*s) return -1;
uint64_t v = 0;
for (const char* p = s; *p; ++p)
{
if (*p < '0' || *p > '9') return -1;
v = v * 10u + (uint32_t)(*p - '0');
if (v > 0xffffffffULL) return -1;
}
*out = (uint32_t)v;
return 0;
}
/**
* @brief Print usage/help information
*/
void cli_print_usage(const char* program_name)
{
printf("Usage: %s [OPTIONS]\n", program_name);
printf("StarForth - A lightweight Forth virtual machine\n\n");
printf("Options:\n");
printf(" -s Script/silent mode - suppress prompts and 'ok' output (for piped input)\n");
printf(" --break-me 🔥 ULTRA-COMPREHENSIVE diagnostic mode - tests EVERYTHING\n");
printf(" --benchmark [N] Run performance benchmarks (default: 1000 iterations)\n");
printf(" --log-error Set logging level to ERROR (only errors)\n");
printf(" --log-warn Set logging level to WARN (warnings and errors)\n");
printf(" --log-info Set logging level to INFO (default)\n");
printf(" --log-test Set logging level to TEST (test results_run_01_2025_12_08 only)\n");
printf(" --log-debug Set logging level to DEBUG (all messages)\n");
printf(" --log-none Disable all logging (maximum performance)\n");
printf(" --fail-fast Stop test suite immediately on first failure\n");
printf(" --doe Run DoE experiment: output CSV metrics to stdout and exit\n");
printf(" --heartbeat-log=<mode> Heartbeat logging mode (off|summary|full)\n");
printf(" off: No heartbeat output (default)\n");
printf(" summary: Bucket aggregates in main CSV\n");
printf(" full: Per-tick CSV to hb/run-<id>.csv\n");
printf(" --disk-img=<path> Use raw disk image file at <path>\n");
printf(" --ram-disk=<MB> RAM fallback size if no --disk-img (default: 1 MB)\n");
printf(" --version, -v Show version information and exit\n");
printf(" --help, -h Show this help message\n\n");
printf("Examples:\n");
printf(" %s # Start REPL with INFO logging\n", program_name);
printf(" %s -s < script.forth # Run script from stdin (no prompts)\n", program_name);
printf(" %s --benchmark # Run benchmarks with 1000 iterations\n", program_name);
printf(" %s --disk-img=./disks/starship.img # Use disk image\n", program_name);
}
/**
* @brief Parse command-line arguments into CLIConfig
*/
CLIConfig cli_parse(int argc, char* argv[])
{
CLIConfig config = {
/* Test modes - all default to 0 */
.run_tests = 0,
.break_me = 0,
.fail_fast = 0,
.run_tests_flag_observed = 0,
/* Benchmark */
.benchmark = 0,
.benchmark_iterations = 1000,
/* Design of Experiments */
.doe_experiment = 0,
.heartbeat_log_mode = HEARTBEAT_LOG_OFF,
/* REPL mode */
.script_mode = 0,
/* Logging */
.log_level = LOG_INFO,
.log_level_explicitly_set = 0,
/* Block I/O */
.disk_img_path = NULL,
.ram_disk_mb = 1,
};
for (int i = 1; i < argc; i++)
{
if (strcmp(argv[i], "--run-tests") == 0)
{
config.run_tests_flag_observed = 1;
}
else if (strcmp(argv[i], "--break-me") == 0)
{
config.break_me = 1;
}
else if (strcmp(argv[i], "--benchmark") == 0)
{
config.benchmark = 1;
if (i + 1 < argc && atoi(argv[i + 1]) > 0)
{
config.benchmark_iterations = atoi(argv[i + 1]);
i++;
}
}
else if (strcmp(argv[i], "--log-error") == 0)
{
config.log_level = LOG_ERROR;
config.log_level_explicitly_set = 1;
}
else if (strcmp(argv[i], "--log-warn") == 0)
{
config.log_level = LOG_WARN;
config.log_level_explicitly_set = 1;
}
else if (strcmp(argv[i], "--log-info") == 0)
{
config.log_level = LOG_INFO;
config.log_level_explicitly_set = 1;
}
else if (strcmp(argv[i], "--log-test") == 0)
{
config.log_level = LOG_TEST;
config.log_level_explicitly_set = 1;
}
else if (strcmp(argv[i], "--log-debug") == 0)
{
config.log_level = LOG_DEBUG;
config.log_level_explicitly_set = 1;
}
else if (strcmp(argv[i], "--log-none") == 0)
{
config.log_level = LOG_NONE;
config.log_level_explicitly_set = 1;
}
else if (strcmp(argv[i], "--fail-fast") == 0)
{
config.fail_fast = 1;
}
else if (strcmp(argv[i], "--doe-experiment") == 0 || strcmp(argv[i], "--doe") == 0)
{
config.doe_experiment = 1;
}
else if (strncmp(argv[i], "--heartbeat-log=", 16) == 0)
{
const char* mode_str = argv[i] + 16;
if (strcmp(mode_str, "off") == 0) {
config.heartbeat_log_mode = HEARTBEAT_LOG_OFF;
} else if (strcmp(mode_str, "summary") == 0) {
config.heartbeat_log_mode = HEARTBEAT_LOG_SUMMARY;
} else if (strcmp(mode_str, "full") == 0) {
config.heartbeat_log_mode = HEARTBEAT_LOG_FULL;
} else {
fprintf(stderr, "Invalid --heartbeat-log value: %s (expected off|summary|full)\n", mode_str);
exit(1);
}
}
else if (strcmp(argv[i], "-s") == 0)
{
config.script_mode = 1;
}
else if (strncmp(argv[i], "--disk-img=", 11) == 0)
{
config.disk_img_path = argv[i] + 11;
}
else if (strncmp(argv[i], "--ram-disk=", 11) == 0)
{
uint32_t tmp = 0;
if (parse_u32(argv[i] + 11, &tmp) != 0 || tmp == 0)
{
fprintf(stderr, "Invalid --ram-disk value: %s (positive MB expected)\n", argv[i] + 11);
exit(1);
}
config.ram_disk_mb = tmp;
}
else if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0)
{
printf("%s\n", STARFORTH_VERSION_FULL);
exit(0);
}
else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0)
{
cli_print_usage(argv[0]);
exit(0);
}
else
{
printf("Unknown option: %s\n", argv[i]);
cli_print_usage(argv[0]);
exit(1);
}
}
return config;
}
+382
View File
@@ -0,0 +1,382 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "compudynamics.h"
#include <string.h>
#include <stdlib.h>
/* ============================================================================
* Per-Level Tuning Accessors
* ============================================================================
*
* Returned by value, not exposed as addressable globals -- see the
* "Tuning knobs are returned by value" comment in compudynamics.h for why.
*/
/* Matches the constants ssm_jacquard.c/.h used before this module existed
* (SSM_SCORE_MAX, SSM_SCORE_DECAY_FACTOR_Q16, SSM_UCB_K, SSM_REWARD_GAIN,
* SSM_JOINT_WEIGHT_Q16, SSM_ANOVA_WEIGHT_Q16, SSM_ENTROPY/CV/TEMPORAL_DECAY
* thresholds) -- a lift, not a retune. id_window_depth=32 and
* locality_lookback=8 are new: they size the entity-ID history the
* deterministic locality signal replaces temporal_decay with. */
CDTuning cd_tuning_word(void)
{
CDTuning t;
/* classifier */
t.id_window_depth = 32u;
t.locality_lookback = 8u;
t.diversity_high_threshold_q16 = 49152u; /* 0.75 * 65536 */
t.volatility_high_threshold_q16 = 9830u; /* 0.15 * 65536 */
t.locality_high_threshold_q16 = 32768u; /* 0.5 * 65536 */
/* bandit */
t.num_configs = 128u; /* 7-bit L1-L7 */
t.num_regimes = 8u; /* 3-bit diversity/volatility/locality */
t.score_max = 65535u;
t.score_min = 0u;
t.score_decay_factor_q16 = 64881u; /* 0.99 * 65536 */
t.ucb_k = 6554u; /* 10% of score_max */
t.reward_gain = 16384u; /* 25% of score_max */
t.joint_weight_q16 = 49152u; /* 0.75 */
t.anova_weight_q16 = 16384u; /* 0.25 */
return t;
}
/* VM level: classifier-side parameters only, matching capsule_vm_physics.c's
* existing VM_FLEET_WINDOW_DEPTH so a future migration has a ready-made
* starting point. Bandit fields are zero and unused -- see compudynamics.h. */
CDTuning cd_tuning_vm(void)
{
CDTuning t;
/* classifier */
t.id_window_depth = 64u; /* matches VM_FLEET_WINDOW_DEPTH */
t.locality_lookback = 16u;
t.diversity_high_threshold_q16 = 49152u;
t.volatility_high_threshold_q16 = 9830u;
t.locality_high_threshold_q16 = 32768u;
/* bandit -- unused at VM level today */
t.num_configs = 0u;
t.num_regimes = 0u;
t.score_max = 0u;
t.score_min = 0u;
t.score_decay_factor_q16 = 0u;
t.ucb_k = 0u;
t.reward_gain = 0u;
t.joint_weight_q16 = 0u;
t.anova_weight_q16 = 0u;
return t;
}
/* ============================================================================
* Integer Helpers (isqrt, ln approximation)
* ============================================================================
*/
static uint32_t cd_isqrt32(uint32_t n)
{
uint32_t x, y;
if (n == 0u) return 0u;
x = n;
y = (x + 1u) / 2u;
while (y < x) {
x = y;
y = (x + n / x) / 2u;
}
return x;
}
static uint64_t cd_isqrt64(uint64_t n)
{
uint64_t x, y;
if (n == 0u) return 0u;
x = n;
y = (x + 1u) / 2u;
while (y < x) {
x = y;
y = (x + n / x) / 2u;
}
return x;
}
/* floor(log2(n)) * ln(2) * 65536, integer shifts only. See ssm_jacquard.c's
* former ln_approx_q16 -- identical algorithm, moved here so the generic
* bandit doesn't depend on ssm_jacquard.c. */
static uint32_t cd_ln_approx_q16(uint32_t n)
{
uint32_t bits, tmp;
if (n <= 1u) return 0u;
bits = 0u;
tmp = n >> 1u;
while (tmp > 0u) { tmp >>= 1u; bits++; }
return bits * 45426u;
}
/* ============================================================================
* Deterministic Regime Classification
* ============================================================================
*/
CDRegimeMetrics cd_classify_ids(const uint32_t *ids, uint32_t count,
uint32_t locality_lookback)
{
CDRegimeMetrics m;
uint32_t n, i, j, distinct, lookback;
uint8_t counted[CD_MAX_CLASSIFY_DEPTH];
uint32_t per_id_count[CD_MAX_CLASSIFY_DEPTH];
m.diversity_q16 = 0u;
m.volatility_q16 = 0u;
m.locality_q16 = 0u;
if (!ids || count == 0u) return m;
n = (count > CD_MAX_CLASSIFY_DEPTH) ? CD_MAX_CLASSIFY_DEPTH : count;
memset(counted, 0, n * sizeof(counted[0]));
/* --- Diversity: distinct IDs / n, and per-ID touch counts for the
* volatility pass below (single O(n^2) sweep, n <= 64) --- */
distinct = 0u;
for (i = 0u; i < n; i++) {
uint32_t c;
if (counted[i]) continue;
c = 1u;
counted[i] = 1u;
for (j = i + 1u; j < n; j++) {
if (ids[j] == ids[i]) { c++; counted[j] = 1u; }
}
per_id_count[distinct] = c;
distinct++;
}
m.diversity_q16 = (uint32_t)(((uint64_t)distinct * 65536u) / n);
/* --- Volatility: coefficient of variation of per-ID touch counts,
* computed in Q8 to avoid overflow, result in Q16 --- */
if (distinct > 0u) {
uint32_t mean_q8 = (n << 8u) / distinct;
uint64_t sum_var_q16 = 0u;
uint64_t stddev_q8, cv_q16;
for (i = 0u; i < distinct; i++) {
int64_t diff_q8 = (int64_t)(per_id_count[i] << 8u) - (int64_t)mean_q8;
uint64_t diff2_q16 = (uint64_t)(diff_q8 * diff_q8);
sum_var_q16 += diff2_q16;
}
stddev_q8 = cd_isqrt64(sum_var_q16 / distinct);
cv_q16 = (mean_q8 > 0u) ? ((stddev_q8 * 65536u) / mean_q8) : 0u;
m.volatility_q16 = (cv_q16 > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t)cv_q16;
}
/* --- Locality: fraction of touches (from lookback..n-1) that repeat
* an ID seen within the preceding lookback touches --- */
lookback = (locality_lookback > n) ? n : locality_lookback;
if (n > lookback && lookback > 0u) {
uint32_t matches = 0u;
uint32_t denom = n - lookback;
for (i = lookback; i < n; i++) {
for (j = i - lookback; j < i; j++) {
if (ids[j] == ids[i]) { matches++; break; }
}
}
m.locality_q16 = (uint32_t)(((uint64_t)matches * 65536u) / denom);
}
return m;
}
/* ============================================================================
* Generic UCB1 Config-Space Bandit
* ============================================================================
*/
int cd_config_table_init(CDConfigTable *table, const CDTuning *tuning)
{
if (!table || !tuning) return -1;
memset(table, 0, sizeof(*table));
if (tuning->num_configs == 0u || tuning->num_regimes == 0u) return -1;
table->entries = (CDConfigEntry *)malloc(tuning->num_configs * sizeof(CDConfigEntry));
table->regime_scores = (uint32_t *)malloc(
(size_t)tuning->num_regimes * tuning->num_configs * sizeof(uint32_t));
table->regime_ticks = (uint32_t *)malloc(
(size_t)tuning->num_regimes * tuning->num_configs * sizeof(uint32_t));
table->total_regime_ticks = (uint32_t *)malloc(tuning->num_regimes * sizeof(uint32_t));
if (!table->entries || !table->regime_scores || !table->regime_ticks ||
!table->total_regime_ticks) {
cd_config_table_free(table);
return -1;
}
table->num_configs = tuning->num_configs;
table->num_regimes = tuning->num_regimes;
return 0;
}
void cd_config_table_free(CDConfigTable *table)
{
if (!table) return;
free(table->entries);
free(table->regime_scores);
free(table->regime_ticks);
free(table->total_regime_ticks);
memset(table, 0, sizeof(*table));
}
void cd_config_table_seed(CDConfigTable *table, const CDTuning *tuning,
const uint32_t *initial_scores, uint8_t initial_config)
{
uint32_t c, r;
if (!table || !table->entries || !tuning || !initial_scores) return;
for (c = 0u; c < table->num_configs; c++) {
CDConfigEntry *e = &table->entries[c];
e->config_bits = (uint8_t)c;
e->has_prev = 0u;
e->_pad[0] = 0u;
e->_pad[1] = 0u;
e->benefit_score = initial_scores[c];
e->tick_count = 0u;
e->prev_window = 0u;
e->prev_locality_q16 = 0u;
for (r = 0u; r < table->num_regimes; r++) {
table->regime_scores[r * table->num_configs + c] = initial_scores[c];
table->regime_ticks[r * table->num_configs + c] = 0u;
}
}
table->current_config = initial_config;
table->current_regime = 0u;
for (r = 0u; r < table->num_regimes; r++) {
table->total_regime_ticks[r] = 0u;
}
(void)tuning;
}
uint8_t cd_config_table_tick(CDConfigTable *table, const CDTuning *tuning,
uint8_t regime, uint32_t anova_stability_q16,
int use_joint_convergence,
uint32_t current_window, uint32_t current_locality_q16)
{
uint32_t r, c;
CDConfigEntry *entry;
uint32_t combined_stability;
uint32_t *score_cell;
uint32_t old_score, decayed, new_score;
int64_t delta_score, new_s;
uint32_t total, ln_total, best_c, best_ucb, ci;
if (!table || !table->entries || !tuning) return 0u;
table->current_regime = regime;
r = (uint32_t)regime;
c = (uint32_t)table->current_config;
entry = &table->entries[c];
if (use_joint_convergence && entry->has_prev) {
uint32_t dw_raw = (current_window > entry->prev_window)
? (current_window - entry->prev_window)
: (entry->prev_window - current_window);
uint32_t dl_raw = (current_locality_q16 > entry->prev_locality_q16)
? (current_locality_q16 - entry->prev_locality_q16)
: (entry->prev_locality_q16 - current_locality_q16);
uint64_t dw_q16 = (entry->prev_window > 0u)
? (((uint64_t)dw_raw * 65536u) / entry->prev_window)
: 0u;
uint64_t dl_q16 = dl_raw; /* already Q16, bounded to [0,65536] */
uint64_t sum_sq, joint_err;
uint32_t stability;
if (dw_q16 > 65536u) dw_q16 = 65536u;
if (dl_q16 > 65536u) dl_q16 = 65536u;
/* joint_error = sqrt(dw^2 + dl^2) / sqrt(2), normalised to [0,65536] */
sum_sq = dw_q16 * dw_q16 + dl_q16 * dl_q16;
joint_err = (cd_isqrt64(sum_sq) * 65536u) / 92682u; /* sqrt(2)*65536 ~= 92682 */
if (joint_err > 65536u) joint_err = 65536u;
stability = (uint32_t)(65536u - (uint32_t)joint_err);
combined_stability = (uint32_t)(
((uint64_t)tuning->joint_weight_q16 * stability +
(uint64_t)tuning->anova_weight_q16 * anova_stability_q16) / 65536u
);
} else {
combined_stability = anova_stability_q16;
}
delta_score = ((int64_t)combined_stability - 32768) *
(int64_t)tuning->reward_gain / 32768;
score_cell = &table->regime_scores[r * table->num_configs + c];
old_score = *score_cell;
decayed = (uint32_t)(((uint64_t)old_score * tuning->score_decay_factor_q16) / 65536u);
new_s = (int64_t)decayed + delta_score;
if (new_s < (int64_t)tuning->score_min) new_score = tuning->score_min;
else if (new_s > (int64_t)tuning->score_max) new_score = tuning->score_max;
else new_score = (uint32_t)new_s;
*score_cell = new_score;
table->regime_ticks[r * table->num_configs + c]++;
if (table->total_regime_ticks[r] < 0xFFFFFFFFu) table->total_regime_ticks[r]++;
entry->tick_count++;
entry->prev_window = current_window;
entry->prev_locality_q16 = current_locality_q16;
entry->has_prev = 1u;
/* --- UCB1: select best config for the next tick --- */
total = table->total_regime_ticks[r];
ln_total = cd_ln_approx_q16(total > 0u ? total : 1u);
best_c = 0u;
best_ucb = 0u;
for (ci = 0u; ci < table->num_configs; ci++) {
uint32_t nc = table->regime_ticks[r * table->num_configs + ci];
uint32_t ucb;
if (nc == 0u) {
ucb = tuning->score_max;
} else {
uint32_t ratio = ln_total / nc;
uint32_t sqrt_r = cd_isqrt32(ratio);
uint32_t bonus = (uint32_t)(((uint64_t)tuning->ucb_k * sqrt_r) / 256u);
uint32_t sc = table->regime_scores[r * table->num_configs + ci];
ucb = sc + bonus;
if (ucb > tuning->score_max) ucb = tuning->score_max;
}
if (ucb > best_ucb) { best_ucb = ucb; best_c = ci; }
}
table->current_config = (uint8_t)best_c;
return table->entries[best_c].config_bits;
}
void cd_config_table_force(CDConfigTable *table, uint8_t config_idx)
{
if (!table || !table->entries) return;
if ((uint32_t)config_idx >= table->num_configs) return;
table->current_config = config_idx;
}
+400
View File
@@ -0,0 +1,400 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
*** StarForth ***
dictionary_heat_optimization.c - Phase 2: Heat-Aware Dictionary Lookup Optimization
Implements inference-driven dictionary lookup optimizations based on execution
heat patterns. Uses rolling window to understand which words are hot and how they
co-execute, then reorganizes the dictionary to optimize lookup performance.
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "vm.h"
#include "rolling_window_of_truth.h"
#include "dictionary_heat_optimization.h"
#include "log.h"
#include "platform_time.h"
/* sf_fc_list/sf_fc_count/sf_fc_cap are declared in dictionary_heat_optimization.h */
/* ============================================================================
* Heat Percentile Calculation
* ============================================================================
*/
/**
* @brief qsort comparator for @c cell_t execution-heat values in ascending order.
*
* Compares two @c cell_t values passed as @c const @c void* pointers (the
* signature required by @c qsort). Returns −1, 0, or +1 following C standard
* three-way comparison convention. Used exclusively by
* @c dict_update_heat_percentiles() to sort the scratch @c heats[] array
* before computing the 25th, 50th, and 75th percentile thresholds.
*
* @param a Pointer to the first @c cell_t value.
* @param b Pointer to the second @c cell_t value.
* @return −1 if @p *a < @p *b; +1 if @p *a > @p *b; 0 if equal.
*/
static int compare_heat_ascending(const void *a, const void *b) {
cell_t heat_a = *(const cell_t *)a;
cell_t heat_b = *(const cell_t *)b;
if (heat_a < heat_b) return -1;
if (heat_a > heat_b) return 1;
return 0;
}
/**
* @brief Collect dictionary execution heats and compute the 25th, 50th, and 75th percentiles.
*
* Traverses the full linked-list dictionary under @c vm->dict_lock to snapshot
* every word's @c execution_heat into a stack-allocated scratch array (up to
* @c DICTIONARY_SIZE entries). After releasing the lock it sorts the snapshot
* with @c qsort + @c compare_heat_ascending and computes percentile indices
* using integer arithmetic:
* @code
* vm->heat_threshold_25th = heats[(count * 25) / 100];
* vm->heat_threshold_50th = heats[(count * 50) / 100];
* vm->heat_threshold_75th = heats[(count * 75) / 100];
* @endcode
*
* If the dictionary contains no words all three thresholds are set to 0.
* The lock is held only during traversal; sorting is performed outside the
* critical section to minimise contention with the concurrent heartbeat decay
* sweep that also walks the word list.
*
* The resulting thresholds are consumed by @c dict_find_word_heat_aware()
* (which stratifies its search by heat tier) and by
* @c dict_adaptive_optimization_pass() (which calls this function as its
* first step every ~1 Hz).
*
* @param vm The VM whose dictionary is sampled and whose
* @c heat_threshold_25th / @c heat_threshold_50th /
* @c heat_threshold_75th fields are updated. No-op if NULL.
*/
void dict_update_heat_percentiles(VM *vm) {
if (!vm) return;
/* Collect all heats from dictionary */
cell_t heats[DICTIONARY_SIZE];
int count = 0;
/* Protect traversal against concurrent heartbeat decay sweeps */
sf_mutex_lock(&vm->dict_lock);
for (DictEntry *w = vm->latest; w && count < DICTIONARY_SIZE; w = w->link) {
heats[count++] = w->execution_heat;
}
sf_mutex_unlock(&vm->dict_lock);
if (count == 0) {
vm->heat_threshold_25th = 0;
vm->heat_threshold_50th = 0;
vm->heat_threshold_75th = 0;
return;
}
/* Sort heats in ascending order */
qsort(heats, count, sizeof(cell_t), compare_heat_ascending);
/* Calculate percentiles */
vm->heat_threshold_25th = heats[(count * 25) / 100];
vm->heat_threshold_50th = heats[(count * 50) / 100];
vm->heat_threshold_75th = heats[(count * 75) / 100];
log_message(LOG_DEBUG,
"Heat percentiles: 25th=%ld, 50th=%ld, 75th=%ld (from %d words)",
vm->heat_threshold_25th,
vm->heat_threshold_50th,
vm->heat_threshold_75th,
count);
}
/* ============================================================================
* Heat-Aware Lookup Strategy
* ============================================================================
*/
/**
* @brief Three-pass heat-stratified dictionary lookup, hottest tier first.
*
* Searches the first-character bucket @c sf_fc_list[(unsigned char)name[0]]
* for a word whose @c name and @c name_len match @p name / @p len, scanning
* in three priority tiers ordered by execution heat:
*
* -# **Hot pass** (heat ≥ @c heat_threshold_75th): words in the top 25%
* by execution frequency. Most FORTH programs spend the vast majority of
* ticks in a handful of hot primitives; checking this tier first yields a
* near-constant-time hit on steady-state workloads.
* -# **Medium pass** (heat in [threshold_25th, threshold_75th)): the middle
* 50% of words. These are occasionally-called words that still appear often
* enough to warrant a dedicated pass before touching the long tail.
* -# **Cold pass** (heat < @c heat_threshold_25th): the rarely-executed
* bottom quartile. Words that have never been called also live here.
*
* Within each pass, matching uses the same cheap-first filter chain as the
* standard lookup: @c name_len comparison → last-character byte check →
* full @c memcmp. The last-character pre-filter is skipped for single-character
* names.
*
* Returns @c NULL only if the word is absent from the bucket; all three passes
* exhausted without a hit. Also returns @c NULL immediately if @p vm, @p name,
* or @p len are invalid.
*
* Heat thresholds are updated lazily by @c dict_adaptive_optimization_pass()
* (≈1 Hz); stale thresholds degrade to a full three-pass scan without
* affecting correctness.
*
* @param vm The VM; provides @c heat_threshold_* fields and the bucket table.
* @param name Word name to search for (not NUL-terminated; length in @p len).
* @param len Length of @p name in bytes; must be ≥ 1.
* @return Pointer to the matching @c DictEntry, or @c NULL if not found.
*/
DictEntry *dict_find_word_heat_aware(VM *vm, const char *name, size_t len) {
if (!vm || !name || len == 0) return NULL;
const unsigned char first = (unsigned char)name[0];
DictEntry **bucket = sf_fc_list[first];
size_t n = sf_fc_count[first];
if (!bucket || n == 0) return NULL;
/* Resolution must be arbitrated: the heat-stratified three-pass scan
* returned the first name match in tier order, which resolves shadowed
* (redefined) names by execution heat instead of definition age — a
* FORTH-79 semantics violation that dict_reorganize_buckets_by_heat()
* turns into a live fault (see VM-COUNT primitive/constant collision,
* logs/20260704-155745). The strategy selection between naive and
* heat-aware lookup is preserved; both now resolve through the same
* age-honoring scan. A stratified fast path may be reintroduced on top
* of the resolver if lookup latency ever warrants it. */
return vm_dict_resolve_in_bucket(vm, bucket, n, name, len);
}
/* ============================================================================
* Bucket Reorganization by Heat Patterns
* ============================================================================
*/
/**
* @brief qsort comparator for @c DictEntry* pointers in descending execution-heat order.
*
* Dereferences two @c DictEntry** pointers and compares their
* @c execution_heat fields, returning −1 if the first entry is hotter,
* +1 if the second is hotter, or 0 on a tie. Used exclusively by
* @c dict_reorganize_buckets_by_heat() to sort each of the 256 first-character
* hash buckets so that hot words appear at lower array indices, reducing the
* average scan depth during the medium and cold passes of
* @c dict_find_word_heat_aware().
*
* @param a Pointer to a @c DictEntry* (first element).
* @param b Pointer to a @c DictEntry* (second element).
* @return −1 if @p a is hotter; +1 if @p b is hotter; 0 if equal.
*/
static int compare_dict_entry_heat_desc(const void *a, const void *b) {
DictEntry *entry_a = *(DictEntry * const *)a;
DictEntry *entry_b = *(DictEntry * const *)b;
if (entry_a->execution_heat > entry_b->execution_heat) return -1;
if (entry_a->execution_heat < entry_b->execution_heat) return 1;
return 0;
}
/**
* @brief Sort all 256 first-character hash buckets by descending execution heat.
*
* Holds @c vm->dict_lock for the entire operation to prevent concurrent
* @c DictEntry frees (from word redefinition or GC) from invalidating the
* pointer array while it is being sorted. For each non-empty bucket in
* @c sf_fc_list[0..255]:
*
* 1. Reads @c sf_fc_count[bucket_id] and @c sf_fc_cap[bucket_id]; skips the
* bucket if @c count > @c cap (indicates a TOCTOU race — logs a warning).
* 2. Allocates a temporary copy of the bucket pointer array.
* 3. Re-verifies @c count has not changed (TOCTOU double-check); if it has,
* frees the copy and skips.
* 4. Sorts the copy with @c qsort + @c compare_dict_entry_heat_desc so that
* the hottest @c DictEntry* comes first.
* 5. Copies the sorted array back over the original bucket.
* 6. Frees the temporary copy.
*
* After returning, @c vm->last_bucket_reorg_ns is updated to the completion
* timestamp so that @c dict_adaptive_optimization_pass() can enforce its 1 Hz
* throttle. The elapsed reorganization time is emitted at @c LOG_DEBUG.
*
* @param vm The VM whose first-character hash buckets are reorganized.
* No-op if NULL. @c dict_lock is held during the entire scan.
*/
void dict_reorganize_buckets_by_heat(VM *vm) {
if (!vm) return;
uint64_t reorg_start = sf_monotonic_ns();
/* Race fix: Hold dict_lock during entire reorganization (2025-12-09)
* Without this, main thread can free DictEntry objects while we're sorting pointers */
sf_mutex_lock(&vm->dict_lock);
/* Reorganize each bucket by heat (hottest words first) */
for (int bucket_id = 0; bucket_id < 256; bucket_id++) {
if (!sf_fc_list[bucket_id] || sf_fc_count[bucket_id] == 0) continue;
/* ASan fix: Verify count doesn't exceed capacity before accessing (2025-12-09)
* Race condition: Main thread may have changed count/capacity while we read it */
size_t count = sf_fc_count[bucket_id];
size_t cap = sf_fc_cap[bucket_id];
if (count > cap) {
log_message(LOG_WARN,
"dict_reorganize_buckets_by_heat: skipping bucket %d - count=%zu exceeds capacity=%zu (race condition)",
bucket_id, count, cap);
continue;
}
/* Make a copy of bucket pointers */
DictEntry **bucket_copy = malloc(count * sizeof(DictEntry *));
if (!bucket_copy) continue;
/* Re-verify count hasn't changed (TOCTOU protection) */
size_t count_recheck = sf_fc_count[bucket_id];
if (count_recheck != count || count_recheck > cap) {
log_message(LOG_WARN,
"dict_reorganize_buckets_by_heat: bucket %d count changed (%zu -> %zu), aborting",
bucket_id, count, count_recheck);
free(bucket_copy);
continue;
}
memcpy(bucket_copy, sf_fc_list[bucket_id], count * sizeof(DictEntry *));
/* Sort by descending heat (use cached count to avoid TOCTOU race) */
qsort(bucket_copy, count, sizeof(DictEntry *),
compare_dict_entry_heat_desc);
/* Copy back to original bucket (still use cached count) */
memcpy(sf_fc_list[bucket_id], bucket_copy, count * sizeof(DictEntry *));
free(bucket_copy);
}
sf_mutex_unlock(&vm->dict_lock);
uint64_t reorg_end = sf_monotonic_ns();
vm->last_bucket_reorg_ns = reorg_end;
log_message(LOG_DEBUG,
"Dictionary buckets reorganized by heat in %.1f μs",
(double)(reorg_end - reorg_start) / 1000.0);
}
/* ============================================================================
* Adaptive Optimization Pass
* ============================================================================
*/
/**
* @brief Throttled adaptive optimization pass — runs at most once per second.
*
* Called from the interpreter loop (or heartbeat) on every word dispatch; the
* 1 Hz gate (@c now_ns - @c vm->last_bucket_reorg_ns < 1,000,000,000) ensures
* the three expensive sub-steps run at a rate that is imperceptible to FORTH
* execution while still keeping the dictionary layout fresh:
*
* 1. **Update heat percentiles** — calls @c dict_update_heat_percentiles() to
* re-sample @c execution_heat across all words and compute the 25th / 50th /
* 75th thresholds used by the stratified lookup.
*
* 2. **Reorganize buckets** — calls @c dict_reorganize_buckets_by_heat() to
* sort each of the 256 first-character hash buckets so that the hottest
* words sit at the lowest array indices, shortening the average scan depth
* during the hot-tier pass.
*
* 3. **Select lookup strategy** — queries the rolling window for the current
* pattern-diversity score via @c rolling_window_measure_diversity():
* - Diversity > 70 (high churn in which words are called) → strategy 1
* (@c heat-aware lookup, @c dict_find_word_heat_aware()), where the heat
* stratification most pays off.
* - Diversity ≤ 70 (stable repetitive pattern) → strategy 0 (naive
* newest-first bucket scan), which is already nearly O(1) for programs
* that call the same small word set repeatedly.
* The chosen strategy is stored in @c vm->lookup_strategy and read by the
* interpreter's dictionary lookup dispatch.
*
* @param vm The VM to optimise. No-op if NULL or if less than 1 second has
* elapsed since the last pass (@c vm->last_bucket_reorg_ns).
*/
void dict_adaptive_optimization_pass(VM *vm) {
if (!vm) return;
uint64_t now_ns = sf_monotonic_ns();
/* Run optimization every 1 second */
if (now_ns - vm->last_bucket_reorg_ns < 1000000000ULL) {
return; /* Too soon, skip */
}
/* Step 1: Update heat percentiles based on current dictionary state */
dict_update_heat_percentiles(vm);
/* Step 2: Reorganize buckets by heat order (hot words first) */
dict_reorganize_buckets_by_heat(vm);
/* Step 3: Select lookup strategy based on pattern diversity */
uint64_t pattern_diversity = rolling_window_measure_diversity(&vm->rolling_window);
/* High diversity (0.7+): Use heat-aware lookup (patterns change frequently) */
if (pattern_diversity > 70) {
vm->lookup_strategy = 1; /* Heat-aware */
log_message(LOG_DEBUG,
"High pattern diversity (%.0f%%), using heat-aware lookup",
(double)pattern_diversity);
}
/* Medium diversity: Use naive newest-first (patterns reasonably stable) */
else {
vm->lookup_strategy = 0; /* Naive */
log_message(LOG_DEBUG,
"Stable patterns (%.0f%% diversity), using naive lookup",
(double)pattern_diversity);
}
}
+600
View File
@@ -0,0 +1,600 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* StarForth dictionary_management.c — FORTH-79 + ANSI C99
* Optimized: incremental FC index + capacity reuse + newest-first search
*/
#include "../include/vm.h"
#include "../include/io.h"
#include "../include/log.h"
#include "../include/physics_metadata.h"
#include "../include/physics_hotwords_cache.h"
#include "../include/physics_pipelining_metrics.h"
#include "../include/dictionary_heat_optimization.h"
#include "../include/platform_alloc.h"
#include <string.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium_words.h" /* item 4.1: FORGET coherence hook */
#include "starkernel/vm_uuid.h" /* vm_uuid_hera() */
#endif
/* LIKELY/UNLIKELY macros are defined in vm.h */
#ifndef SF_FC_BUCKETS
#define SF_FC_BUCKETS 256
#endif
/* Buckets of entry pointers, sizes, and capacities (we reuse memory). */
/* Note: sf_fc_list, sf_fc_count, and sf_fc_cap are not static to allow access from
* dictionary_heat_optimization.c for heat-aware bucket reorganization with proper
* capacity checks (ASan fix: 2025-12-09). */
DictEntry **sf_fc_list[SF_FC_BUCKETS];
size_t sf_fc_count[SF_FC_BUCKETS];
size_t sf_fc_cap[SF_FC_BUCKETS];
static DictEntry *sf_cached_latest = NULL; /* last head we indexed */
static uint32_t vm_dictionary_acquire_word_id(VM *vm) {
if (!vm) {
return WORD_ID_INVALID;
}
if (vm->recycled_word_id_count > 0) {
return vm->recycled_word_ids[--vm->recycled_word_id_count];
}
if (vm->next_word_id < DICTIONARY_SIZE) {
return vm->next_word_id++;
}
return WORD_ID_INVALID;
}
void vm_dictionary_track_entry(VM *vm, DictEntry *entry) {
if (!vm || !entry) {
return;
}
uint32_t word_id = vm_dictionary_acquire_word_id(vm);
entry->word_id = word_id;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) {
log_message(LOG_WARN,
"vm_dictionary_track_entry: out of stable IDs, disabling metrics for '%.*s'",
(int)entry->name_len, entry->name);
return;
}
vm->word_id_map[word_id] = entry;
}
void vm_dictionary_untrack_entry(VM *vm, DictEntry *entry) {
if (!vm || !entry) {
return;
}
/* Cache coherence: the entry is about to be removed (FORGET frees it) —
* a stale pointer left in the hot-words ring would be a use-after-free,
* and an older same-named word may become visible again.
* §17.3 (item 4.1): retired under __STARKERNEL__ -- the kernel word
* layer stays inert to this mechanism entirely, so there is nothing to
* keep coherent here on the kernel side. Hosted is unaffected. */
#ifndef __STARKERNEL__
hotwords_cache_evict_entry(vm->hotwords_cache, entry);
#endif
uint32_t word_id = entry->word_id;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) {
return;
}
#ifdef __STARKERNEL__
/* item 4.1 coherence hook: if this word_id is resident on the Stadium,
* reclaim its cell (crediting heat back to the reservoir) BEFORE the id
* is recycled below -- otherwise the next word assigned this same id
* would alias onto the forgotten word's stale cell (same failure class
* as the 2026-08-02 block_words.c aliasing bug). */
stadium_word_forget(vm->stadium_vm_id, word_id);
#endif
if (vm->word_id_map[word_id] == entry) {
vm->word_id_map[word_id] = NULL;
}
if (vm->recycled_word_id_count < DICTIONARY_SIZE) {
vm->recycled_word_ids[vm->recycled_word_id_count++] = word_id;
}
entry->word_id = WORD_ID_INVALID;
}
DictEntry *vm_dictionary_lookup_by_word_id(VM *vm, uint32_t word_id) {
if (!vm || word_id >= DICTIONARY_SIZE) {
return NULL;
}
return vm->word_id_map[word_id];
}
int vm_dict_entry_ok(VM *vm, DictEntry *candidate) {
if (!vm || !candidate) {
return 0;
}
int found = 0;
sf_mutex_lock(&vm->dict_lock);
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
if (e == candidate) {
found = 1;
break;
}
}
sf_mutex_unlock(&vm->dict_lock);
return found;
}
/* --- helpers ------------------------------------------------------------- */
static inline void *sf_xrealloc(void *p, size_t nbytes) {
void *r = sf_realloc(p, nbytes);
if (!r) {
sf_free(p);
return NULL;
}
return r;
}
static void sf_fc_clear_all(void) {
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
sf_free(sf_fc_list[i]);
sf_fc_list[i] = NULL;
sf_fc_count[i] = 0;
sf_fc_cap[i] = 0;
}
}
/* Ensure bucket i has at least `need` capacity; grow geometrically. */
static int sf_fc_reserve(size_t i, size_t need) {
if (sf_fc_cap[i] >= need) return 1;
size_t newcap = sf_fc_cap[i] ? sf_fc_cap[i] : 8;
while (newcap < need) newcap <<= 1;
DictEntry **old = sf_fc_list[i];
DictEntry **newv = (DictEntry **) sf_xrealloc(old, newcap * sizeof(*newv));
if (!newv) return 0;
/* Kernel sf_realloc is a bump allocator that does not copy; patch that up.
* On hosted Linux, realloc already copied before freeing old — do NOT memcpy
* from freed memory (UB; glibc overwrites freed block with heap metadata). */
#ifdef __STARKERNEL__
{
size_t old_count = sf_fc_count[i];
if (newv != old && old && old_count > 0)
memcpy(newv, old, old_count * sizeof(*newv));
}
#endif
sf_fc_list[i] = newv;
sf_fc_cap[i] = newcap;
return 1;
}
/* Full rebuild: count → reserve → fill oldest→newest (so backward scan = newest-first). */
static void sf_rebuild_fc_index(VM *vm) {
/* Reset counts, keep capacity to avoid churn. */
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) sf_fc_count[i] = 0;
/* Count first chars. */
for (DictEntry *e = vm->latest; e; e = e->link) {
unsigned fc = (unsigned char) e->name[0];
sf_fc_count[fc]++;
}
/* Reserve memory once per bucket. */
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) {
if (sf_fc_count[i] && !sf_fc_reserve(i, sf_fc_count[i])) {
/* OOM for this bucket → treat as empty; we’ll just miss the fast path. */
sf_fc_count[i] = 0;
}
}
/* Back-fill so [0]=oldest, [count-1]=newest. */
size_t fill_at[SF_FC_BUCKETS];
for (size_t i = 0; i < SF_FC_BUCKETS; ++i) fill_at[i] = sf_fc_count[i];
for (DictEntry *e = vm->latest; e; e = e->link) {
unsigned fc = (unsigned char) e->name[0];
if (sf_fc_count[fc] == 0 || !sf_fc_list[fc]) continue;
sf_fc_list[fc][--fill_at[fc]] = e;
}
sf_cached_latest = vm->latest;
}
/* Fast path: if exactly one new word was pushed, append it without rebuilding. */
static void sf_try_fast_append(VM *vm) {
if (!vm || vm->latest == sf_cached_latest) return;
DictEntry *newest = vm->latest;
/* single push if the new head links to the previously cached head */
if (newest && newest->link == sf_cached_latest) {
unsigned fc = (unsigned char) newest->name[0];
size_t n = sf_fc_count[fc];
if (sf_fc_reserve(fc, n + 1)) {
/* Keep oldest→…→newest ordering: append at the end. */
sf_fc_list[fc][n] = newest;
sf_fc_count[fc] = n + 1;
sf_cached_latest = newest;
return;
}
}
/* Otherwise fall back to a full rebuild (multiple changes, deletes, etc.). */
sf_rebuild_fc_index(vm);
}
/* --- API ----------------------------------------------------------------- */
/**
* @brief Return whichever of two entries is the more recent definition.
*
* Walks the definition chain from @c vm->latest; the first of (@p a, @p b)
* encountered is the newer one. The link chain is the only reliable age
* order — word_id values are recycled and bucket positions are shuffled by
* @c dict_reorganize_buckets_by_heat(), so neither can arbitrate age.
* Only invoked when a bucket holds two visible entries with the same name
* (redefinition/shadowing), so the walk cost is off the common path.
*/
static DictEntry *dict_newer_of(VM *vm, DictEntry *a, DictEntry *b) {
for (DictEntry *e = vm->latest; e; e = e->link) {
if (e == a) return a;
if (e == b) return b;
}
return a;
}
DictEntry *vm_dict_resolve_in_bucket(VM *vm, DictEntry **bucket, size_t n,
const char *name, size_t len) {
if (!vm || !bucket || n == 0 || !name || len == 0) return NULL;
const unsigned char last = (len > 1) ? (unsigned char) name[len - 1] : 0;
DictEntry *best = NULL;
/* Full scan: bucket order is heat-shuffled, so no prefix of the bucket
* is authoritative — every same-named candidate must be considered and
* the newest visible definition wins (FORTH-79 shadowing). */
for (size_t i = n; i-- > 0;) {
DictEntry *e = bucket[i];
if (!e) continue;
if ((size_t) e->name_len != len) continue;
#ifdef WORD_HIDDEN
if (UNLIKELY(e->flags & WORD_HIDDEN)) continue;
#endif
#ifdef WORD_SMUDGED
if (UNLIKELY(e->flags & WORD_SMUDGED)) continue;
#endif
if (len > 1 && (unsigned char) e->name[len - 1] != last) continue;
if (memcmp(e->name, name, len) != 0) continue;
best = best ? dict_newer_of(vm, best, e) : e;
}
return best;
}
/* Find by name: newest-first within the first-character bucket. */
/**
* @brief Finds a word in the dictionary by name
*
* Searches for a word in the dictionary using an optimized first-character index.
* The search is performed newest-first within the matching first-character bucket.
*
* @param vm The virtual machine context
* @param name The name to search for
* @param len Length of the name string
* @return DictEntry* Pointer to found dictionary entry or NULL if not found
*/
DictEntry *vm_find_word(VM *vm, const char *name, size_t len) {
if (UNLIKELY(!vm || !name || len == 0)) return NULL;
/* DoE counter: dictionary lookups */
vm->heartbeat.dictionary_lookups++;
/* Thread safety: Acquire dict_lock for read access to dictionary */
sf_mutex_lock(&vm->dict_lock);
/* Keep the index in sync with dictionary head, cheaply if possible. */
if (UNLIKELY(sf_cached_latest != vm->latest)) sf_try_fast_append(vm);
const unsigned char first = (unsigned char) name[0];
DictEntry **bucket = sf_fc_list[first];
size_t n = sf_fc_count[first];
if (UNLIKELY(!bucket || n == 0)) {
sf_mutex_unlock(&vm->dict_lock);
return NULL;
}
/* Use hot-words cache for physics-driven frequency-based acceleration.
* The cache's bucket fallback resolves via vm_dict_resolve_in_bucket(),
* so a non-NULL result already honors newest-first shadowing.
* §17.3 (item 4.1): bypassed under __STARKERNEL__ -- the kernel side
* feeds the Stadium instead (vm_core.c's dispatch sites), and this
* lookup fast path is retired in favor of the ordinary bucket scan
* below. Hosted is unaffected. */
#ifndef __STARKERNEL__
DictEntry *found = hotwords_cache_lookup(vm, vm->hotwords_cache, bucket, n, name, len);
if (found) {
sf_mutex_unlock(&vm->dict_lock);
return found;
}
#endif
/* Phase 2: Choose lookup strategy based on pattern diversity */
if (vm->lookup_strategy == 1) {
/* Heat-aware lookup: search by heat percentiles (hot first) */
DictEntry *result = dict_find_word_heat_aware(vm, name, len);
sf_mutex_unlock(&vm->dict_lock);
return result;
}
/* Fallback/Default: arbitrated scan — buckets may be heat-reordered, so
* first-match-wins is unsound; the resolver picks the newest visible
* definition among all same-named candidates. */
DictEntry *e = vm_dict_resolve_in_bucket(vm, bucket, n, name, len);
sf_mutex_unlock(&vm->dict_lock);
return e;
}
/* Create a new word (unchanged layout); index append happens lazily on next lookup. */
/**
* @brief Creates a new word in the dictionary
*
* Allocates and initializes a new dictionary entry with the given name and function.
* The word is added to the dictionary but index update is deferred until next lookup.
*
* @param vm The virtual machine context
* @param name Name for the new word
* @param len Length of the name string
* @param func Function pointer for the word's behavior
* @return DictEntry* Pointer to new dictionary entry or NULL on failure
*/
DictEntry *vm_create_word(VM *vm, const char *name, size_t len, word_func_t func) {
if (!vm || !name || len == 0 || len > WORD_NAME_MAX) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_create_word: invalid parameters (vm=%p, name=%p, len=%zu, max=%d)",
(void *) vm, (const void *) name, len, WORD_NAME_MAX);
}
return NULL;
}
DictEntry *entry = NULL;
int lock_held = 0;
sf_mutex_lock(&vm->dict_lock);
lock_held = 1;
/* Pin is permanent: no word may shadow a pinned entry — ever, by anyone */
for (DictEntry *scan = vm->latest; scan; scan = scan->link) {
if (scan->acl_pinned && scan->name_len == (uint8_t)len &&
memcmp(scan->name, name, len) == 0) {
vm->error = 1;
log_message(LOG_WARN, "vm_create_word: cannot shadow pinned word '%.*s'",
(int)len, name);
entry = NULL;
goto create_cleanup;
}
}
size_t base = offsetof(DictEntry, name);
size_t name_bytes = len + 1;
size_t align = sizeof(cell_t);
size_t df_off = (base + name_bytes + (align - 1)) & ~(align - 1);
size_t total = df_off + sizeof(cell_t);
entry = (DictEntry *) sf_malloc(total);
if (!entry) {
vm->error = 1;
log_message(LOG_ERROR, "vm_create_word: malloc failed for '%.*s' (%zu bytes)",
(int) len, name, total);
entry = NULL;
goto create_cleanup;
}
entry->link = vm->latest;
entry->func = func;
entry->flags = 0;
entry->name_len = (uint8_t) len;
entry->execution_heat = 0; /* Initialize execution heat counter */
entry->acl_default = ACL_USER_DEFAULT;
entry->acl_ttl = 0; /* first execution always rechecks */
entry->acl_allow = 1; /* permissive until ACL.4th loads */
entry->acl_mode = ACL_MODE_TTL;
entry->acl_pinned = 0;
entry->word_id = WORD_ID_INVALID;
uint32_t header_bytes = (total > UINT32_MAX) ? UINT32_MAX : (uint32_t) total;
physics_metadata_init(entry, header_bytes);
physics_metadata_apply_seed(entry);
/* Initialize word transition metrics for pipelining (Phase 1) */
entry->transition_metrics = (WordTransitionMetrics *)sf_malloc(sizeof(WordTransitionMetrics));
if (entry->transition_metrics) {
transition_metrics_init(entry->transition_metrics);
} else {
log_message(LOG_WARN, "vm_create_word: transition metrics malloc failed for '%.*s'", (int)len, name);
}
memcpy(entry->name, name, len);
entry->name[len] = '\0';
if (df_off > base + name_bytes) {
memset(((uint8_t *) entry) + base + name_bytes, 0, df_off - (base + name_bytes));
}
*(cell_t *) (((uint8_t *) entry) + df_off) = 0;
vm->latest = entry;
vm_dictionary_track_entry(vm, entry);
/* Cache coherence: this definition may shadow an older word of the same
* name that the hot-words cache is still serving. Evict the name so the
* next lookup re-resolves through the arbitrated bucket scan.
* §17.3 (item 4.1): retired under __STARKERNEL__, same as the other
* hotwords_cache_* call sites in this file. */
#ifndef __STARKERNEL__
hotwords_cache_evict_name(vm->hotwords_cache, name, len);
#endif
log_message(LOG_DEBUG, "vm_create_word: '%.*s' len=%zu total=%zu @%p",
(int) len, name, len, total, (void *) entry);
create_cleanup:
if (lock_held)
sf_mutex_unlock(&vm->dict_lock);
return entry;
}
/* Linear scan by func (left as-is; usually cold path). */
/**
* @brief Finds a word by its function pointer
*
* Performs a linear scan of the dictionary to find an entry with matching function.
*
* @param vm The virtual machine context
* @param func Function pointer to search for
* @return DictEntry* Pointer to found dictionary entry or NULL if not found
*/
DictEntry *vm_dictionary_find_by_func(VM *vm, word_func_t func) {
for (DictEntry *e = vm ? vm->latest : NULL; e; e = e->link) {
if (e->func == func) return e;
}
return NULL;
}
DictEntry *vm_dictionary_find_latest_by_func(VM *vm, word_func_t func) {
return vm_dictionary_find_by_func(vm, func);
}
/**
* @brief Gets pointer to a word's data field
*
* Calculates and returns pointer to the aligned data field following the name field.
*
* @param entry Dictionary entry to get data field from
* @return cell_t* Pointer to data field or NULL if entry is NULL
*/
cell_t *vm_dictionary_get_data_field(DictEntry *entry) {
if (!entry) return NULL;
size_t base = offsetof(DictEntry, name);
size_t name_bytes = (size_t) entry->name_len + 1;
size_t align = sizeof(cell_t);
size_t df_off = (base + name_bytes + (align - 1)) & ~(align - 1);
return (cell_t *) (((uint8_t *) entry) + df_off);
}
/**
* @brief Marks the latest word as hidden
*
* Sets the WORD_HIDDEN flag on the most recently defined word.
*
* @param vm The virtual machine context
*/
void vm_hide_word(VM *vm) {
if (vm && vm->latest) {
vm->latest->flags |= WORD_HIDDEN;
physics_metadata_refresh_state(vm->latest);
/* Visibility changed: this name may now resolve to an older entry.
* §17.3 (item 4.1): retired under __STARKERNEL__. */
#ifndef __STARKERNEL__
hotwords_cache_evict_name(vm->hotwords_cache,
vm->latest->name, vm->latest->name_len);
#endif
}
}
void vm_smudge_word(VM *vm) {
if (!vm || !vm->latest) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_smudge_word: invalid vm or no latest word");
}
return;
}
vm->latest->flags ^= WORD_SMUDGED;
physics_metadata_refresh_state(vm->latest);
/* Visibility changed in either direction: force re-resolution.
* §17.3 (item 4.1): retired under __STARKERNEL__. */
#ifndef __STARKERNEL__
hotwords_cache_evict_name(vm->hotwords_cache,
vm->latest->name, vm->latest->name_len);
#endif
}
void vm_pin_execution_heat(VM *vm) {
if (!vm || !vm->latest) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_pin_execution_heat: invalid vm or no latest word");
}
return;
}
vm->latest->flags |= WORD_PINNED;
physics_metadata_refresh_state(vm->latest);
}
void vm_unpin_execution_heat(VM *vm) {
if (!vm || !vm->latest) {
if (vm) {
vm->error = 1;
log_message(LOG_ERROR, "vm_unpin_execution_heat: invalid vm or no latest word");
}
return;
}
vm->latest->flags &= ~WORD_PINNED;
physics_metadata_refresh_state(vm->latest);
}
/* If you ever need to hard-reset (e.g., switching vocabularies wholesale) */
/**
* @brief Resets the dictionary index
*
* Clears all index data and cached state. Used when switching vocabularies.
*/
void vm_dictionary_index_reset(void) {
sf_cached_latest = NULL;
sf_fc_clear_all();
}
+639
View File
@@ -0,0 +1,639 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file doe_metrics.c
* @brief Design of Experiments metrics collection implementation
*/
#include "doe_metrics.h"
#include "physics_hotwords_cache.h"
#include "rolling_window_of_truth.h"
#include "rolling_window_knobs.h"
#include "inference_engine.h"
#include "platform_time.h"
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef __unix__
#include <unistd.h>
#endif
/* Forward declarations from physics system */
extern struct {
uint64_t total_lookups;
uint64_t cache_hits;
uint64_t bucket_hits;
} physics_global_stats;
/**
* Get current CPU temperature in Celsius
*/
int32_t metrics_get_cpu_temp_c(void) {
#ifdef __unix__
FILE *f = fopen("/sys/class/thermal/thermal_zone0/temp", "r");
if (!f) return 0;
int temp_millidegrees = 0;
if (fscanf(f, "%d", &temp_millidegrees) != 1) {
fclose(f);
return 0;
}
fclose(f);
return (int32_t)(temp_millidegrees / 1000);
#else
return 0;
#endif
}
/**
* Get current CPU frequency in MHz
*/
int32_t metrics_get_cpu_freq_mhz(void) {
#ifdef __unix__
/* Try scaling_cur_freq first */
FILE *f = fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", "r");
if (f) {
int freq_khz = 0;
if (fscanf(f, "%d", &freq_khz) == 1) {
fclose(f);
return (int32_t)(freq_khz / 1000);
}
fclose(f);
}
/* Fallback to /proc/cpuinfo */
f = fopen("/proc/cpuinfo", "r");
if (f) {
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "cpu MHz", 7) == 0) {
float mhz = 0.0f;
if (sscanf(line, "cpu MHz : %f", &mhz) == 1) {
fclose(f);
return (int32_t)mhz;
}
}
}
fclose(f);
}
#endif
return 0;
}
/**
* Get current timestamp as ISO 8601 string
*/
void metrics_get_timestamp(char *buf, size_t bufsize) {
if (bufsize < 32) return;
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
strftime(buf, bufsize, "%Y-%m-%dT%H:%M:%S", tm_info);
}
/**
* Extract metrics from VM hotwords cache stats
*/
static void extract_cache_metrics(const HotwordsCache *cache, DoeMetrics *metrics) {
if (!cache) {
metrics->cache_hits = 0;
metrics->cache_hit_percent = 0.0;
metrics->bucket_hits = 0;
metrics->bucket_hit_percent = 0.0;
metrics->cache_hit_latency_ns = 0;
metrics->cache_hit_stddev_ns = 0;
metrics->bucket_search_latency_ns = 0;
metrics->bucket_search_stddev_ns = 0;
return;
}
const HotwordsStats *stats = &cache->stats;
/* Cache hits */
metrics->cache_hits = stats->cache_hits;
if (stats->total_lookups > 0) {
metrics->cache_hit_percent = 100.0 * (double)stats->cache_hits / (double)stats->total_lookups;
} else {
metrics->cache_hit_percent = 0.0;
}
/* Bucket hits */
metrics->bucket_hits = stats->bucket_hits;
if (stats->total_lookups > 0) {
metrics->bucket_hit_percent = 100.0 * (double)stats->bucket_hits / (double)stats->total_lookups;
} else {
metrics->bucket_hit_percent = 0.0;
}
/* Cache hit latency (convert from Q48.16 to ns) */
if (stats->cache_hit_samples > 0) {
int64_t avg_q48 = stats->cache_hit_total_ns_q48 / (int64_t)stats->cache_hit_samples;
metrics->cache_hit_latency_ns = avg_q48 >> 16; /* Convert from Q48.16 to nanoseconds */
/* StdDev calculation from variance sum */
if (stats->cache_hit_samples > 1) {
/* Simplified: use variance sum for estimation */
int64_t variance_q48 = stats->cache_hit_variance_sum_q48 / (int64_t)stats->cache_hit_samples;
metrics->cache_hit_stddev_ns = (int64_t)(variance_q48 >> 16);
} else {
metrics->cache_hit_stddev_ns = 0;
}
} else {
metrics->cache_hit_latency_ns = 0;
metrics->cache_hit_stddev_ns = 0;
}
/* Bucket search latency (convert from Q48.16 to ns) */
if (stats->bucket_search_samples > 0) {
int64_t avg_q48 = stats->bucket_search_total_ns_q48 / (int64_t)stats->bucket_search_samples;
metrics->bucket_search_latency_ns = avg_q48 >> 16; /* Convert from Q48.16 to nanoseconds */
/* StdDev calculation from variance sum */
if (stats->bucket_search_samples > 1) {
int64_t variance_q48 = stats->bucket_search_variance_sum_q48 / (int64_t)stats->bucket_search_samples;
metrics->bucket_search_stddev_ns = (int64_t)(variance_q48 >> 16);
} else {
metrics->bucket_search_stddev_ns = 0;
}
} else {
metrics->bucket_search_latency_ns = 0;
metrics->bucket_search_stddev_ns = 0;
}
}
/**
* Extract metrics from entire VM
*/
DoeMetrics metrics_from_vm(VM *vm, uint64_t workload_duration_ns,
int32_t cpu_temp_delta_c, int32_t cpu_freq_delta_mhz) {
DoeMetrics metrics = {0};
/* Lookups */
metrics.total_lookups = vm->hotwords_cache ? vm->hotwords_cache->stats.total_lookups : 0;
/* Cache metrics */
if (ENABLE_HOTWORDS_CACHE && vm->hotwords_cache) {
extract_cache_metrics(vm->hotwords_cache, &metrics);
metrics.enable_hotwords_cache = vm->hotwords_cache->enabled ? 1 : 0;
} else {
metrics.enable_hotwords_cache = 0;
}
/* Pipelining metrics - extract from global pipeline metrics (Loop #4) */
metrics.context_predictions_total = vm->pipeline_metrics.prefetch_attempts;
metrics.context_correct = vm->pipeline_metrics.prefetch_hits;
metrics.context_accuracy_percent = 0.0;
if (vm->pipeline_metrics.prefetch_attempts > 0) {
metrics.context_accuracy_percent = 100.0 * (double)vm->pipeline_metrics.prefetch_hits /
(double)vm->pipeline_metrics.prefetch_attempts;
}
/* === Rolling Window Metrics (Loop #2) === */
metrics.window_diversity_percent = 0.0;
metrics.window_final_size_bytes = 4096;
metrics.rolling_window_width = (uint32_t)vm->rolling_window.effective_window_size;
metrics.actual_window_size = (uint32_t)(vm->rolling_window.total_executions < ROLLING_WINDOW_SIZE
? vm->rolling_window.total_executions
: ROLLING_WINDOW_SIZE);
metrics.total_executions = vm->rolling_window.total_executions;
/* Protect access to last_inference_outputs against heartbeat thread race */
sf_mutex_lock(&vm->tuning_lock);
metrics.window_variance_q48 = vm->last_inference_outputs ?
vm->last_inference_outputs->window_variance_q48 : 0;
sf_mutex_unlock(&vm->tuning_lock);
/* === Heat Dynamics (Loop #1 & #3) === */
metrics.decay_slope = (double)vm->decay_slope_q48 / 65536.0;
/* Collect snapshot of current dictionary state */
{
uint64_t hot_word_count = 0;
uint64_t stale_word_count = 0;
uint64_t total_heat = 0;
uint32_t word_count = 0;
sf_mutex_lock(&vm->dict_lock);
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
hot_word_count++;
else if (e->execution_heat > 0 && e->execution_heat < 10)
stale_word_count++;
total_heat += e->execution_heat;
word_count++;
}
sf_mutex_unlock(&vm->dict_lock);
metrics.total_heat = total_heat;
metrics.hot_word_count = hot_word_count;
metrics.stale_word_count = stale_word_count;
metrics.stale_word_ratio = (word_count > 0) ? (double)stale_word_count / (double)word_count : 0.0;
metrics.avg_word_heat = (word_count > 0) ? (double)total_heat / (double)word_count : 0.0;
}
/* === Heartbeat & Timing (Loop #7) === */
metrics.tick_count = vm->heartbeat.tick_count;
metrics.tick_target_ns = vm->heartbeat.tick_target_ns;
metrics.inference_run_count = vm->heartbeat.inference_run_count;
metrics.early_exit_count = vm->heartbeat.early_exit_count;
/* === Cache Promotions/Demotions (Loop #4) === */
if (vm->hotwords_cache) {
metrics.cache_promotions = vm->hotwords_cache->stats.promotions;
metrics.cache_demotions = vm->hotwords_cache->stats.evictions; /* evictions = demotions */
} else {
metrics.cache_promotions = 0;
metrics.cache_demotions = 0;
}
/* === Window & Decay Inference (Loop #5 & #6) === */
metrics.prefetch_accuracy_percent = 0.0;
metrics.prefetch_attempts = vm->pipeline_metrics.prefetch_attempts;
metrics.prefetch_hits = vm->pipeline_metrics.prefetch_hits;
metrics.window_tuning_checks = vm->pipeline_metrics.window_tuning_checks;
metrics.final_effective_window_size = (uint32_t)vm->rolling_window.effective_window_size;
if (vm->pipeline_metrics.prefetch_attempts > 0) {
metrics.prefetch_accuracy_percent = 100.0 * (double)vm->pipeline_metrics.prefetch_hits /
(double)vm->pipeline_metrics.prefetch_attempts;
}
/* === Performance counters === */
metrics.words_executed = vm->heartbeat.words_executed;
metrics.dictionary_lookups = vm->heartbeat.dictionary_lookups;
/* Performance - workload duration as Q48.16 */
metrics.vm_workload_duration_ns_q48 = (int64_t)workload_duration_ns << 16;
metrics.total_runtime_ms = 0;
metrics.memory_allocated_bytes = 0;
metrics.speedup_vs_baseline = 1.0;
/* Statistical (defaults) */
metrics.ci_lower_95 = 0.0;
metrics.ci_upper_95 = 0.0;
/* System state deltas - convert to Q48.16
* UBSan fix: Use multiplication instead of left-shift for signed values (2025-12-09)
* Left-shifting negative values is undefined behavior */
metrics.cpu_temp_delta_c_q48 = (int64_t)cpu_temp_delta_c * 65536;
metrics.cpu_freq_delta_mhz_q48 = (int64_t)cpu_freq_delta_mhz * 65536;
/* Tuning knobs */
metrics.decay_rate_q16 = DECAY_RATE_PER_US_Q16;
metrics.decay_min_interval_ns = DECAY_MIN_INTERVAL;
metrics.rolling_window_size = ROLLING_WINDOW_SIZE;
metrics.adaptive_shrink_rate = 75;
metrics.heat_cache_demotion_threshold = 10;
/* === Loop Enable Flags (2^7 factorial) === */
/* L8 FINAL INTEGRATION: All loops always-on, controlled by L8 policy */
metrics.enable_loop_1_heat_tracking = 1; /* Always-on (internal signal) */
metrics.enable_loop_2_rolling_window = 1; /* Always-on (L8-controlled) */
metrics.enable_loop_3_linear_decay = 1; /* Always-on */
metrics.enable_loop_4_pipelining = 1; /* Always-on (internal signal) */
metrics.enable_loop_5_window_inference = 1; /* Always-on */
metrics.enable_loop_6_decay_inference = 1; /* Always-on */
metrics.enable_loop_7_adaptive_heartrate = 1; /* Always-on */
/* Legacy configuration */
metrics.enable_hotwords_cache = ENABLE_HOTWORDS_CACHE;
metrics.enable_pipelining = ENABLE_PIPELINING;
return metrics;
}
/**
* Write CSV header
*/
void metrics_write_csv_header(FILE *out) {
fprintf(out,
/* Loop enable flags (2^7 factorial) - FIRST for easy filtering */
"L1_heat,L2_window,L3_decay,L4_pipeline,L5_win_inf,L6_decay_inf,L7_heartrate,"
/* Cache stats */
"total_lookups,cache_hits,cache_hit_pct,bucket_hits,bucket_hit_pct,"
"cache_lat_ns,cache_lat_std,bucket_lat_ns,bucket_lat_std,"
/* Pipelining (Loop #4) */
"ctx_pred_total,ctx_correct,ctx_acc_pct,cache_promos,cache_demos,"
/* Rolling window (Loop #2) */
"win_diversity_pct,win_final_bytes,win_width,actual_win_size,win_total_exec,win_var_q48,"
/* Heat dynamics (Loop #1 & #3) */
"decay_slope,total_heat,hot_words,stale_words,stale_ratio,avg_heat,"
/* Heartbeat & timing (Loop #7) */
"tick_count,tick_target_ns,infer_runs,early_exits,"
/* Window & decay inference (Loop #5 & #6) */
"prefetch_acc_pct,prefetch_attempts,prefetch_hits,win_tune_checks,final_win_size,"
/* Performance */
"workload_ns_q48,runtime_ms,words_exec,dict_lookups,mem_bytes,speedup,"
/* Statistical */
"ci_lower_95,ci_upper_95,"
/* System deltas */
"cpu_temp_delta_q48,cpu_freq_delta_q48,"
/* Tuning knobs */
"decay_rate_q16,decay_min_ns,roll_win_size,shrink_rate,demo_thresh,"
/* Legacy */
"hotwords_cache,pipelining\n");
}
/**
* Write CSV row - MUST match header column order exactly
*/
void metrics_write_csv_row(FILE *out, const DoeMetrics *metrics) {
fprintf(out,
/* Loop enable flags (2^7 factorial) - FIRST for easy filtering */
"%d,%d,%d,%d,%d,%d,%d,"
/* Cache stats */
"%u,%lu,%.2f,%lu,%.2f,"
"%ld,%ld,%ld,%ld,"
/* Pipelining (Loop #4) */
"%lu,%lu,%.2f,%lu,%lu,"
/* Rolling window (Loop #2) */
"%.2f,%u,%u,%u,%lu,%lu,"
/* Heat dynamics (Loop #1 & #3) */
"%.6f,%lu,%lu,%lu,%.6f,%.6f,"
/* Heartbeat & timing (Loop #7) */
"%lu,%lu,%lu,%lu,"
/* Window & decay inference (Loop #5 & #6) */
"%.2f,%lu,%lu,%lu,%u,"
/* Performance */
"%ld,%lu,%lu,%lu,%lu,%.4f,"
/* Statistical */
"%.6f,%.6f,"
/* System deltas */
"%ld,%ld,"
/* Tuning knobs */
"%u,%u,%u,%u,%u,"
/* Legacy */
"%d,%d\n",
/* Loop enable flags */
metrics->enable_loop_1_heat_tracking,
metrics->enable_loop_2_rolling_window,
metrics->enable_loop_3_linear_decay,
metrics->enable_loop_4_pipelining,
metrics->enable_loop_5_window_inference,
metrics->enable_loop_6_decay_inference,
metrics->enable_loop_7_adaptive_heartrate,
/* Cache stats */
metrics->total_lookups,
metrics->cache_hits,
metrics->cache_hit_percent,
metrics->bucket_hits,
metrics->bucket_hit_percent,
metrics->cache_hit_latency_ns,
metrics->cache_hit_stddev_ns,
metrics->bucket_search_latency_ns,
metrics->bucket_search_stddev_ns,
/* Pipelining (Loop #4) */
metrics->context_predictions_total,
metrics->context_correct,
metrics->context_accuracy_percent,
metrics->cache_promotions,
metrics->cache_demotions,
/* Rolling window (Loop #2) */
metrics->window_diversity_percent,
metrics->window_final_size_bytes,
metrics->rolling_window_width,
metrics->actual_window_size,
metrics->total_executions,
metrics->window_variance_q48,
/* Heat dynamics (Loop #1 & #3) */
metrics->decay_slope,
metrics->total_heat,
metrics->hot_word_count,
metrics->stale_word_count,
metrics->stale_word_ratio,
metrics->avg_word_heat,
/* Heartbeat & timing (Loop #7) */
metrics->tick_count,
metrics->tick_target_ns,
metrics->inference_run_count,
metrics->early_exit_count,
/* Window & decay inference (Loop #5 & #6) */
metrics->prefetch_accuracy_percent,
metrics->prefetch_attempts,
metrics->prefetch_hits,
metrics->window_tuning_checks,
metrics->final_effective_window_size,
/* Performance */
metrics->vm_workload_duration_ns_q48,
metrics->total_runtime_ms,
metrics->words_executed,
metrics->dictionary_lookups,
metrics->memory_allocated_bytes,
metrics->speedup_vs_baseline,
/* Statistical */
metrics->ci_lower_95,
metrics->ci_upper_95,
/* System deltas */
metrics->cpu_temp_delta_c_q48,
metrics->cpu_freq_delta_mhz_q48,
/* Tuning knobs */
metrics->decay_rate_q16,
metrics->decay_min_interval_ns,
metrics->rolling_window_size,
metrics->adaptive_shrink_rate,
metrics->heat_cache_demotion_threshold,
/* Legacy */
metrics->enable_hotwords_cache,
metrics->enable_pipelining);
}
/**
* Print metrics as human-readable text
*/
void metrics_print_text(FILE *out, const DoeMetrics *metrics) {
fprintf(out, "\n=== DoE Metrics ===\n");
fprintf(out, "Lookups: %u\n", metrics->total_lookups);
fprintf(out, "Cache Hits: %lu (%.2f%%)\n", metrics->cache_hits, metrics->cache_hit_percent);
fprintf(out, "Bucket Hits: %lu (%.2f%%)\n", metrics->bucket_hits, metrics->bucket_hit_percent);
fprintf(out, "Hit Latency: %ld ns (±%ld)\n", metrics->cache_hit_latency_ns, metrics->cache_hit_stddev_ns);
fprintf(out, "Search Latency: %ld ns (±%ld)\n", metrics->bucket_search_latency_ns, metrics->bucket_search_stddev_ns);
fprintf(out, "Predictions: %lu / %lu (%.2f%% accurate)\n",
metrics->context_correct, metrics->context_predictions_total, metrics->context_accuracy_percent);
fprintf(out, "Window Width: %u bytes\n", metrics->rolling_window_width);
fprintf(out, "Decay Slope: %.2f\n", metrics->decay_slope);
fprintf(out, "Workload Time: %ld ns (Q48.16)\n", metrics->vm_workload_duration_ns_q48);
fprintf(out, "CPU Temp Delta: %ld°C (Q48.16)\n", metrics->cpu_temp_delta_c_q48);
fprintf(out, "CPU Freq Delta: %ld MHz (Q48.16)\n", metrics->cpu_freq_delta_mhz_q48);
}
/**
* Integer square root using Newton's method (pure C99, no math.h)
*/
static uint64_t isqrt(uint64_t n) {
if (n == 0) return 0;
if (n < 4) return 1;
uint64_t x = n;
uint64_t y = (x + 1) / 2;
while (y < x) {
x = y;
y = (x + n / x) / 2;
}
return x;
}
/**
* Write reduced James Law CSV row (20 columns for window scaling experiment)
*
* This outputs only the subset of metrics needed for the James Law validation experiment.
* Format matches the header in run_window_sweep.sh (minus the 5 script-prepended columns).
*
* Expected columns (20):
* workload_duration_ns, l8_mode_final, lambda_effective, K_statistic, K_deviation,
* entropy, cv, temporal_decay, stability_score, ns_per_word, total_runtime_ms,
* window_final, hot_word_count, avg_heat, bucket_mean_K, bucket_cv_K,
* bucket_window_var, bucket_heat_var, bucket_mode_transitions, bucket_collapse_flag
*/
void metrics_write_james_law_csv_row(FILE *out, const DoeMetrics *metrics, VM *vm) {
/* Extract rolling window size for calculations */
uint32_t rolling_window_size = metrics->rolling_window_size;
if (rolling_window_size == 0) rolling_window_size = 1; /* Avoid division by zero */
/* Calculate James Law metrics */
uint32_t window_width = metrics->rolling_window_width;
double lambda_effective = (double)window_width;
double K_statistic = (window_width > 0) ? ((double)window_width / (double)rolling_window_size) : 1.0;
double K_deviation = K_statistic - 1.0;
/* Calculate derived metrics */
double entropy = metrics->window_diversity_percent / 100.0; /* Normalize to 0-1 */
/* Coefficient of variation: use cache hit latency stddev / mean as proxy */
double cv_metric = (metrics->cache_hit_latency_ns > 0)
? ((double)metrics->cache_hit_stddev_ns / (double)metrics->cache_hit_latency_ns)
: 1.0;
double temporal_decay = metrics->decay_slope;
/* Stability score: inverse of stale word ratio (higher = more stable) */
double stability_score = (metrics->stale_word_ratio < 1.0)
? (1.0 - metrics->stale_word_ratio) * 10000.0 /* Scale to match expected range */
: 0.0;
/* Performance: nanoseconds per word executed */
double ns_per_word = (metrics->words_executed > 0)
? ((double)metrics->vm_workload_duration_ns_q48 / (double)metrics->words_executed)
: 0.0;
/* Get L8 mode from VM heartbeat state */
uint8_t l8_mode_final = 1; /* Default to mode 1 if unavailable */
if (vm && vm->heartbeat.tick_buffer_write_index > 0) {
/* Get last tick's L8 mode */
uint32_t last_idx = (vm->heartbeat.tick_buffer_write_index - 1) % HEARTBEAT_TICK_BUFFER_SIZE;
l8_mode_final = vm->heartbeat.tick_buffer[last_idx].l8_mode;
}
/* Calculate bucket aggregates from heartbeat state */
double bucket_mean_K = 0.0;
double bucket_cv_K = 0.0;
double bucket_window_var = 0.0;
double bucket_heat_var = 0.0;
uint32_t bucket_mode_transitions = 0;
int bucket_collapse_flag = 0;
if (vm && vm->heartbeat.bucket_tick_count > 0) {
uint32_t n = vm->heartbeat.bucket_tick_count;
/* Mean K */
bucket_mean_K = vm->heartbeat.bucket_sum_K / (double)n;
/* CV of K: sqrt(variance) / mean using integer approximation */
double mean_K_squared = bucket_mean_K * bucket_mean_K;
double variance_K = (vm->heartbeat.bucket_sum_K_squared / (double)n) - mean_K_squared;
if (variance_K > 0.0 && bucket_mean_K > 0.0) {
/* Scale variance to avoid precision loss, compute integer sqrt, then scale back */
uint64_t variance_scaled = (uint64_t)(variance_K * 1000000.0);
uint64_t stddev_scaled = isqrt(variance_scaled);
double stddev_K = (double)stddev_scaled / 1000.0;
bucket_cv_K = (stddev_K / bucket_mean_K) * 100.0; /* As percentage */
}
/* Window and heat variance (using accumulated sums as proxy) */
bucket_window_var = vm->heartbeat.bucket_sum_window_variance;
bucket_heat_var = vm->heartbeat.bucket_sum_heat_variance;
bucket_mode_transitions = vm->heartbeat.bucket_mode_transitions;
bucket_collapse_flag = vm->heartbeat.bucket_collapse_flag;
}
/* Write CSV row: 20 columns */
fprintf(out,
"%ld,%u,%.6f,%.6f,%.6f," /* workload_duration_ns, l8_mode_final, lambda_effective, K_statistic, K_deviation */
"%.6f,%.6f,%.6f,%.6f,%.6f," /* entropy, cv, temporal_decay, stability_score, ns_per_word */
"%lu,%u,%lu,%.6f," /* total_runtime_ms, window_final, hot_word_count, avg_heat */
"%.6f,%.6f,%.6f,%.6f,%u,%d\n", /* bucket_mean_K, bucket_cv_K, bucket_window_var, bucket_heat_var, bucket_mode_transitions, bucket_collapse_flag */
/* Row 1-5 */
metrics->vm_workload_duration_ns_q48,
l8_mode_final,
lambda_effective,
K_statistic,
K_deviation,
/* Row 6-10 */
entropy,
cv_metric,
temporal_decay,
stability_score,
ns_per_word,
/* Row 11-14 */
metrics->total_runtime_ms,
metrics->final_effective_window_size,
metrics->hot_word_count,
metrics->avg_word_heat,
/* Row 15-20 */
bucket_mean_K,
bucket_cv_K,
bucket_window_var,
bucket_heat_var,
bucket_mode_transitions,
bucket_collapse_flag
);
}
+196
View File
@@ -0,0 +1,196 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "vm.h"
#include "log.h"
#include "platform_time.h"
#include <stdio.h>
#include <stdlib.h>
/**
* @brief Capture current VM metrics into a heartbeat tick snapshot
*
* Collects 11 metrics per tick for multivariate dynamics analysis.
* Called every heartbeat tick to record VM state.
*
* @param vm Pointer to the VM instance
* @param snapshot Pointer to snapshot structure to populate
*/
void heartbeat_capture_tick_snapshot(VM* vm, HeartbeatTickSnapshot* snapshot)
{
if (!vm || !snapshot)
return;
HeartbeatState* hb = &vm->heartbeat;
/* Monotonic tick counter — use the real heartbeat counter, not tick_count_total */
snapshot->tick_number = (uint32_t)hb->tick_count;
/* Timing: heartbeats ARE the clock ("we count heartbeats not timer ticks").
* sf_monotonic_ns() is unreliable in freestanding kernel (returns 0 on amd64,
* raw counter on riscv64). Use nominal tick period instead. */
snapshot->elapsed_ns = hb->tick_count * (uint64_t)HEARTBEAT_TICK_NS;
static uint64_t last_tick_count = 0;
uint64_t delta_ticks = hb->tick_count - last_tick_count;
snapshot->tick_interval_ns = delta_ticks * (uint64_t)HEARTBEAT_TICK_NS;
last_tick_count = hb->tick_count;
/* Delta metrics — computed from per-VM counters using static last-value tracking.
* On VM reset (current < last), treat current value as the full delta. */
/* Cache metrics from hotwords cache */
snapshot->cache_hits_delta = 0; /* TODO: wire hotwords cache hit counter */
snapshot->bucket_hits_delta = 0; /* TODO: wire bucket hit counter */
/* Word execution delta — words executed since the previous heartbeat tick.
* vm->heartbeat.words_executed is incremented by vm_core.c on every dispatch. */
{
static uint64_t last_words_executed = 0;
uint64_t cur = hb->words_executed;
snapshot->word_executions_delta = (uint32_t)(cur >= last_words_executed
? cur - last_words_executed
: cur);
last_words_executed = cur;
}
/* Hot word count - walk dictionary and count words above threshold */
snapshot->hot_word_count = 0;
uint64_t total_heat = 0;
uint32_t word_count = 0;
for (DictEntry* entry = vm->latest; entry; entry = entry->link)
{
if (entry->execution_heat > 0)
{
total_heat += entry->execution_heat;
word_count++;
/* Count as "hot" if above cache promotion threshold */
if (entry->execution_heat >= 10) /* HEAT_CACHE_DEMOTION_THRESHOLD */
{
snapshot->hot_word_count++;
}
}
}
/* Average word heat (convert Q48.16 to double) */
if (word_count > 0)
{
snapshot->avg_word_heat = (double)total_heat / (double)word_count / 65536.0;
}
else
{
snapshot->avg_word_heat = 0.0;
}
/* Rolling window size.
* window_width = L8's inferred effective size (the target).
* actual_window_size = min(total_executions, effective_window_size):
* how many entries are ACTUALLY being analysed right now — grows toward
* window_width as data accumulates, then tracks it as L8 narrows/widens. */
snapshot->window_width = vm->rolling_window.effective_window_size;
{
uint64_t fill = vm->rolling_window.total_executions;
uint32_t eff = vm->rolling_window.effective_window_size;
snapshot->actual_window_size = (uint32_t)(fill < (uint64_t)eff ? fill : eff);
}
/* predicted_label_hits: ANOVA early-exit confirmations per tick.
* Each early exit means the inference engine validated the current L8 config
* as stable — the selector's choice correlated with subsequent execution patterns.
* High rate = selector chose well and the system settled.
* Low rate = selector is still searching. */
{
static uint64_t last_early_exit_count = 0;
uint64_t cur = hb->early_exit_count;
snapshot->predicted_label_hits = (uint32_t)(cur >= last_early_exit_count
? cur - last_early_exit_count
: cur);
last_early_exit_count = cur;
}
/* Jitter: tick-based timing has no wall-clock deviation to measure.
* Encode delta_ticks as a deviation: 0 means exactly on schedule (1 tick/call). */
snapshot->estimated_jitter_ns = (delta_ticks > 1)
? (double)((delta_ticks - 1) * (uint64_t)HEARTBEAT_TICK_NS)
: 0.0;
}
/**
* @brief Emit a heartbeat tick snapshot as CSV row to stderr
*
* Outputs 11 comma-separated values (no header) for real-time streaming.
* Called every heartbeat tick to emit metrics immediately.
*
* Format: tick_number,elapsed_ns,tick_interval_ns,cache_hits_delta,
* bucket_hits_delta,word_executions_delta,hot_word_count,
* avg_word_heat,window_width,actual_window_size,predicted_label_hits,estimated_jitter_ns
*
* @param vm Pointer to the VM instance (unused, for API consistency)
* @param snapshot Pointer to the snapshot to emit
*/
void heartbeat_emit_tick_row(VM* vm, HeartbeatTickSnapshot* snapshot)
{
(void)vm; /* Unused parameter */
if (!snapshot)
return;
/* Emit CSV row to stderr - no header, just data */
fprintf(stderr, "%u,%lu,%lu,%u,%u,%u,%lu,%.6f,%u,%u,%u,%.2f\n",
snapshot->tick_number,
snapshot->elapsed_ns,
snapshot->tick_interval_ns,
snapshot->cache_hits_delta,
snapshot->bucket_hits_delta,
snapshot->word_executions_delta,
snapshot->hot_word_count,
snapshot->avg_word_heat,
snapshot->window_width,
snapshot->actual_window_size,
snapshot->predicted_label_hits,
snapshot->estimated_jitter_ns);
/* Ensure immediate output to stderr (unbuffered) */
fflush(stderr);
}
+832
View File
@@ -0,0 +1,832 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <assert.h>
#include "inference_engine.h"
#include "q48_16.h"
#include "vm.h"
#include "rolling_window_of_truth.h"
#include "ssm_jacquard.h"
/* ============================================================================
* Phase 2A: ANOVA Early-Exit Check
* ============================================================================
*
* Purpose: Skip full inference if variance hasn't changed significantly
* Threshold: 5% variance change (VARIANCE_SIGNIFICANCE_THRESHOLD in vm.h)
* Cost if stable: ~100 CPU cycles
* Cost if unstable: Full inference run (~5-10k cycles)
*/
/**
* @brief ANOVA early-exit check: test whether variance has stabilised.
*
* Computes |current - last| / last. If the relative change is <= 5%
* (threshold = 3276 in Q48.16), the variance is considered stable and
* full inference can be skipped (~100 cycles vs ~5–10k for a full run).
* Always returns 0 (unstable) on the first call when @c last_variance == 0.
*
* @param current_variance Variance from the current snapshot (Q48.16)
* @param last_variance Variance from the previous inference run (Q48.16)
* @return 1 if variance is stable (skip inference), 0 if full run needed
*/
static int has_variance_stabilized(
q48_16_t current_variance,
q48_16_t last_variance
)
{
if (last_variance == 0) {
/* First run, always do full inference */
return 0;
}
/* Calculate variance delta as ratio */
q48_16_t delta = (current_variance > last_variance)
? (current_variance - last_variance)
: (last_variance - current_variance);
/* Compute delta / last_variance in Q48.16 */
q48_16_t ratio = q48_div(delta, last_variance);
/* Threshold: 5% = 0.05 in Q48.16 = 0.05 * 65536 = 3276 */
q48_16_t threshold = 3276;
if (ratio <= threshold) {
/* Variance is stable, skip full inference */
return 1;
}
/* Variance changed significantly, run full inference */
return 0;
}
/* ============================================================================
* Phase 2B: Heat Trajectory Extraction
* ============================================================================
*
* Purpose: Fresh snapshot of execution_heat from dictionary
* Strategy: Iterate vm->latest backwards, collect heat values
* Timing: O(dictionary_entries), called every HEARTBEAT_INFERENCE_FREQUENCY ticks
*/
/*
* Build a heat trajectory for inference using a consistent rolling window snapshot.
*
* We linearize the rolling window into a temporary ID buffer (via the public export API)
* and convert the most recent entries into execution_heat samples by consulting the
* stable word-id map protected by dict_lock. This keeps the inference engine fully
* thread-safe while still operating on real heat values instead of raw IDs.
*/
/**
* @brief Build a heat trajectory array from the rolling window of truth.
*
* Linearises the rolling window via @c rolling_window_export_execution_history(),
* then translates each word-id slot to its current @c execution_heat by
* looking up the entry under @c vm->dict_lock. The result is a freshly
* allocated array of @c *out_length uint64_t values in chronological order.
* The caller is responsible for freeing the returned pointer. Returns NULL
* (and sets @c *out_length = 0) on allocation failure or empty window.
*
* @param window Rolling window to export from
* @param vm VM whose dictionary is consulted for heat values
* @param out_length Output: number of elements in the returned array
* @return Heap-allocated trajectory array, or NULL on failure
*/
static uint64_t* extract_heat_trajectory(
RollingWindowOfTruth *window,
VM *vm,
uint64_t *out_length
)
{
if (!window || !vm || !out_length) {
return NULL;
}
uint32_t *word_ids = (uint32_t*)malloc(ROLLING_WINDOW_SIZE * sizeof(uint32_t));
if (!word_ids) {
*out_length = 0;
return NULL;
}
uint64_t exported = rolling_window_export_execution_history(window,
word_ids,
ROLLING_WINDOW_SIZE);
if (exported == 0) {
free(word_ids);
*out_length = 0;
return NULL;
}
uint64_t span = window->is_warm
? (uint64_t)window->effective_window_size
: exported;
if (span > exported) span = exported;
if (span == 0) {
free(word_ids);
*out_length = 0;
return NULL;
}
uint64_t *trajectory = (uint64_t*)malloc(span * sizeof(uint64_t));
if (!trajectory) {
free(word_ids);
*out_length = 0;
return NULL;
}
uint64_t start = exported - span;
sf_mutex_lock(&vm->dict_lock);
for (uint64_t i = 0; i < span; i++) {
uint32_t word_id = word_ids[start + i];
uint64_t heat = 0;
if (word_id < DICTIONARY_SIZE) {
DictEntry *entry = vm_dictionary_lookup_by_word_id(vm, word_id);
if (entry) {
heat = (uint64_t)entry->execution_heat;
}
}
trajectory[i] = heat;
}
sf_mutex_unlock(&vm->dict_lock);
free(word_ids);
*out_length = span;
return trajectory;
}
/* ============================================================================
* Phase 2C: Window Width Inference (Variance Inflection)
* ============================================================================
*
* Purpose: Find statistical point where adding more data stops refining understanding
*
* Algorithm:
* 1. For each sub-window size from MIN to full:
* - Compute variance_q48 of heat in that window
* 2. Detect inflection: where d(variance)/d(size) → 0
* - When |variance[i+1] - variance[i]| < 1% of current variance
* - Return that size as inferred_window_width
* 3. Clamp to [ADAPTIVE_MIN_WINDOW_SIZE, ROLLING_WINDOW_SIZE]
*/
/**
* @brief Compute the variance of a heat trajectory in Q48.16 fixed-point.
*
* Computes the population variance: Σ(heat[i] - mean)² / N.
* All intermediate calculations use Q48.16 arithmetic. Returns 0 for
* an empty array.
*
* @param heat_data Array of execution heat values (raw uint64_t counts)
* @param length Number of elements in @c heat_data
* @return Population variance in Q48.16 format
*/
q48_16_t compute_variance_q48(
const uint64_t *heat_data,
uint64_t length
)
{
if (length == 0) return 0;
/* Compute mean in Q48.16 */
uint64_t sum = 0;
for (uint64_t i = 0; i < length; i++) {
sum += heat_data[i];
}
q48_16_t mean = q48_div(q48_from_u64(sum), q48_from_u64(length));
/* Compute sum of squared deviations */
uint64_t sum_sq_diff = 0;
for (uint64_t i = 0; i < length; i++) {
q48_16_t heat_q48 = q48_from_u64(heat_data[i]);
q48_16_t diff = (heat_q48 > mean) ? (heat_q48 - mean) : (mean - heat_q48);
q48_16_t sq_diff = q48_mul(diff, diff);
sum_sq_diff += q48_to_u64(sq_diff);
}
/* Variance = sum_sq_diff / length */
q48_16_t variance = q48_div(q48_from_u64(sum_sq_diff), q48_from_u64(length));
return variance;
}
/* ============================================================================
* Helper: Compute Median (for Levene's Test)
* ============================================================================
*
* Purpose: Find median of array for robust central tendency
* Note: Uses simple selection algorithm (O(n) expected, O(n²) worst case)
*/
/**
* @brief Compute the median of a Q48.16 array.
*
* Makes a temporary copy, sorts it with bubble sort (acceptable for the
* small chunk counts used in Levene's test, typically K <= 16), and
* returns the middle element. Returns 0 for an empty array.
*
* @param data Array of Q48.16 values
* @param length Number of elements
* @return Median value in Q48.16 format
*/
static q48_16_t compute_median_q48(
const q48_16_t *data,
uint32_t length
)
{
if (length == 0) return 0;
if (length == 1) return data[0];
/* Make a copy and sort (bubble sort for small arrays) */
q48_16_t *sorted = (q48_16_t *)malloc(length * sizeof(q48_16_t));
if (!sorted) return 0;
memcpy(sorted, data, length * sizeof(q48_16_t));
/* Simple bubble sort */
for (uint32_t i = 0; i < length - 1; i++) {
for (uint32_t j = 0; j < length - i - 1; j++) {
if (sorted[j] > sorted[j + 1]) {
q48_16_t tmp = sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = tmp;
}
}
}
q48_16_t median = sorted[length / 2];
free(sorted);
return median;
}
/* ============================================================================
* Helper: Compute Mean in Q48.16
* ============================================================================
*
* Purpose: Calculate arithmetic mean of Q48.16 values
*/
/**
* @brief Compute the arithmetic mean of a Q48.16 array.
*
* Sums the integer parts of each element (>> 16) and returns the result
* converted back to Q48.16. Returns 0 for an empty array.
*
* @param data Array of Q48.16 values
* @param length Number of elements
* @return Arithmetic mean in Q48.16 format
*/
static q48_16_t compute_mean_q48(
const q48_16_t *data,
uint32_t length
)
{
if (length == 0) return 0;
uint64_t sum = 0;
for (uint32_t i = 0; i < length; i++) {
sum += data[i] >> 16; /* Convert to integer part */
}
return q48_from_u64(sum / length);
}
/* ============================================================================
* Levene's Test for Equality of Variance (Statistically Valid)
* ============================================================================
*
* Purpose: Test if multiple samples have equal variance
* Reference: Levene, H. (1960). "Robust tests for equality of variances"
*
* Null Hypothesis H₀: All chunk variances are equal
* Test Statistic W: Ratio of variance of deviations to overall deviation
*
* If W > critical_value (≈6.5 for α=0.05): REJECT H₀ (variances differ)
* If W ≤ critical_value: FAIL TO REJECT H₀ (variances are similar)
*
* Input:
* - chunk_variances: Array of K variance values (one per chunk)
* - num_chunks: K (number of chunks)
* - chunk_size: N (size of each chunk, all equal)
*
* Output:
* - W statistic in Q48.16 format
* - Compare result to LEVENE_CRITICAL_VALUE_Q48
*/
/**
* @brief Compute Levene's W statistic for equality of variance.
*
* Tests the null hypothesis H₀: all chunk variances are equal.
* Uses the median-based variant for robustness: z_i = |var_i - median_var|,
* then W = (K-1)*N*Σ(z_i - z_bar)² / (K*N * var(z)).
* Returns 0 for fewer than 2 chunks. Compare result to
* @c LEVENE_CRITICAL_VALUE_Q48 (≈ 6.5 at α=0.05): W > critical means
* variances differ significantly.
*
* @param chunk_variances Array of K per-chunk variance values (Q48.16)
* @param num_chunks Number of chunks K (must be >= 2)
* @param chunk_size Number of samples N per chunk
* @return Levene's W statistic in Q48.16 format
*/
static q48_16_t compute_levene_statistic(
const q48_16_t *chunk_variances,
uint32_t num_chunks,
uint32_t chunk_size
)
{
if (num_chunks < 2) return 0;
/* Step 1: Compute median variance */
q48_16_t median_var = compute_median_q48(chunk_variances, num_chunks);
/* Step 2: Compute z_i = |variance_i - median_var| */
q48_16_t *z = (q48_16_t *)malloc(num_chunks * sizeof(q48_16_t));
if (!z) return 0;
for (uint32_t i = 0; i < num_chunks; i++) {
z[i] = (chunk_variances[i] > median_var)
? (chunk_variances[i] - median_var)
: (median_var - chunk_variances[i]);
}
/* Step 3: Compute z_bar = mean(z) */
q48_16_t z_bar = compute_mean_q48(z, num_chunks);
/* Step 4: Compute numerator = (K-1) * N * Σ(z_i - z_bar)² */
q48_16_t sum_sq_diff = 0;
for (uint32_t i = 0; i < num_chunks; i++) {
q48_16_t diff = (z[i] > z_bar) ? (z[i] - z_bar) : (z_bar - z[i]);
q48_16_t sq = q48_mul(diff, diff);
sum_sq_diff = q48_add(sum_sq_diff, sq);
}
q48_16_t numerator = q48_mul(
q48_from_u64(num_chunks - 1),
q48_mul(q48_from_u64(chunk_size), sum_sq_diff)
);
/* Step 5: Compute denominator = Σ_i Σ_j (z_ij - z_i_mean)² */
/* Approximation: Use variance of z values */
q48_16_t z_variance = compute_variance_q48((const uint64_t *)z, num_chunks);
q48_16_t denominator = q48_mul(q48_from_u64(num_chunks), z_variance);
/* Step 6: W = numerator / denominator */
q48_16_t W = (denominator > 0) ? q48_div(numerator, denominator) : 0;
free(z);
return W;
}
/**
* @brief Find the minimum window size where heat variance is statistically stable.
*
* Implements Loop #5 (window width inference). Scans candidate window sizes from
* @c ADAPTIVE_MIN_WINDOW_SIZE to the full trajectory in steps of 64, divides the
* trajectory into disjoint chunks of that size, and applies Levene's test. Returns
* the first size where W <= 6.5 (α=0.05 critical value). Falls back to the full
* trajectory length if no size passes. @c full_variance is unused (retained for
* API compatibility with callers that pre-compute it).
*
* @param heat_data Heat trajectory array
* @param trajectory_length Number of elements in @c heat_data
* @param full_variance Unused; retained for API compatibility
* @return Minimum sufficient window width in [ADAPTIVE_MIN_WINDOW_SIZE, ROLLING_WINDOW_SIZE]
*/
uint32_t find_variance_inflection(
const uint64_t *heat_data,
uint64_t trajectory_length,
q48_16_t full_variance /* Unused in new algorithm */
)
{
(void)full_variance;
/* ========================================================================
* REDESIGNED: Levene's Test for Statistical Validity (2025-11-19)
* ========================================================================
*
* OLD ALGORITHM (FLAWED):
* - Computed prefix variance: var[0..N], var[0..2N], var[0..3N], ...
* - Violated statistical independence
* - Confounded "enough data" with "variance decay"
* - Used magic 1% threshold with no statistical justification
*
* NEW ALGORITHM (VALID):
* - Divides trajectory into K disjoint chunks of size N
* - Computes variance of each chunk independently
* - Uses Levene's test for equality of variance
* - Statistically sound hypothesis test (α=0.05)
* - Finds MINIMUM window size where variance is stable
*
* Reference: Levene, H. (1960). "Robust tests for equality of variances"
* In: Contributions to Probability and Statistics
*/
/* Use constants from vm.h (defined as macros) */
#ifndef ADAPTIVE_MIN_WINDOW_SIZE
#define ADAPTIVE_MIN_WINDOW_SIZE 256
#endif
#ifndef ROLLING_WINDOW_SIZE
#define ROLLING_WINDOW_SIZE 4096
#endif
if (trajectory_length == 0) {
return ROLLING_WINDOW_SIZE / 2; /* Default */
}
uint32_t min_size = ADAPTIVE_MIN_WINDOW_SIZE;
uint32_t max_size = (trajectory_length < ROLLING_WINDOW_SIZE)
? (uint32_t)trajectory_length
: ROLLING_WINDOW_SIZE;
/* Levene's critical value for α=0.05 with K≥3 degrees of freedom */
/* Theoretical value ≈ 5.88, conservative estimate ≈ 6.5 */
q48_16_t levene_critical = q48_from_double(6.5);
/* Scan for minimum window size where variance is statistically stable */
for (uint32_t size = min_size; size <= max_size; size += 64) {
uint32_t num_chunks = (uint32_t)(trajectory_length / size);
/* Need at least 3 chunks for reliable statistical test */
if (num_chunks < 3) {
continue; /* Too few chunks, try larger size */
}
/* Allocate and compute variance for each disjoint chunk */
q48_16_t *chunk_vars = (q48_16_t *)malloc(num_chunks * sizeof(q48_16_t));
if (!chunk_vars) {
continue; /* Allocation failed, skip this size */
}
for (uint32_t i = 0; i < num_chunks; i++) {
uint64_t chunk_start = (uint64_t)i * (uint64_t)size;
chunk_vars[i] = compute_variance_q48(
&heat_data[chunk_start],
size
);
}
/* Apply Levene's test for equality of variance */
q48_16_t W = compute_levene_statistic(chunk_vars, num_chunks, size);
free(chunk_vars);
/* If test passes: variances are statistically similar */
/* This window size is SUFFICIENT for capturing the pattern */
if (W <= levene_critical) {
return size; /* Found minimum sufficient window */
}
}
/* If no size passed test, use maximum available */
return max_size;
}
/* ============================================================================
* Phase 2D: Decay Slope Inference (Closed-Form Linear Regression)
* ============================================================================
*
* Purpose: Extract decay_slope from heat trajectory via exponential fitting
*
* Model: ln(heat[t]) = ln(h0) - slope*t
* (Exponential decay: heat(t) = h0 * e^(-slope*t))
*
* Algorithm (Integer-only, Q48.16):
* 1. Transform trajectory to log space
* 2. Linear regression on log_heat = a - slope*t
* 3. Extract slope coefficient
*
* Closed-form solution:
* numerator = n * Σ(t*ln(heat)) - Σt * Σln(heat)
* denominator = n * Σ(t²) - (Σt)²
* slope = numerator / denominator
*/
/**
* @brief Infer the decay slope from a heat trajectory via log-linear regression.
*
* Implements Loop #6 (decay slope inference). Fits the model
* ln(heat[t]) = ln(h₀) - slope*t using the closed-form OLS solution:
* slope = (n*Σ(t·ln(h)) - Σt·Σln(h)) / (n·Σt² - (Σt)²).
* The sign of the numerator is taken as absolute value since decay rate
* is always positive. Zero heat values are skipped. Returns 0 if fewer
* than 2 samples are available.
*
* @param heat_data Array of execution heat values (raw counts)
* @param length Number of elements in @c heat_data
* @return Inferred decay slope in Q48.16 format (0 = insufficient data)
*/
uint64_t infer_decay_slope_q48(
const uint64_t *heat_data,
uint64_t length
)
{
if (length < 2) {
return 0;
}
/* Compute sums for linear regression */
uint64_t n = length;
uint64_t sum_t = (n * (n - 1)) / 2; /* 0+1+2+...+(n-1) */
uint64_t sum_t_sq = (n * (n - 1) * (2 * n - 1)) / 6; /* 0²+1²+...+(n-1)² */
q48_16_t sum_log_heat = 0;
q48_16_t sum_t_log_heat = 0;
for (uint64_t t = 0; t < length; t++) {
if (heat_data[t] == 0) continue; /* Skip zero heat values */
/* Compute ln(heat[t]) in Q48.16 */
q48_16_t log_heat = q48_log_approx(heat_data[t]);
sum_log_heat = q48_add(sum_log_heat, log_heat);
/* Compute t * ln(heat[t]) */
q48_16_t t_log = q48_mul(q48_from_u64(t), log_heat);
sum_t_log_heat = q48_add(sum_t_log_heat, t_log);
}
/* Compute slope = (n*Σ(t*ln) - Σt*Σln) / (n*Σ(t²) - (Σt)²) */
/* Note: For decay, numerator may be negative, so use signed arithmetic */
int64_t n_times_sum_t_log = (int64_t)q48_mul(q48_from_u64(n), sum_t_log_heat);
int64_t sum_t_times_sum_log = (int64_t)q48_mul(q48_from_u64(sum_t), sum_log_heat);
int64_t numerator_signed = n_times_sum_t_log - sum_t_times_sum_log;
/* Take absolute value (decay rate is always positive) */
uint64_t numerator = (numerator_signed < 0) ? (uint64_t)(-numerator_signed) : (uint64_t)numerator_signed;
uint64_t denominator_raw = (n * sum_t_sq) - (sum_t * sum_t);
if (denominator_raw == 0) {
denominator_raw = 1; /* Avoid division by zero */
}
/* Divide: numerator is Q48.16, denominator is raw */
/* slope = numerator_Q48 / denominator_raw preserves Q48.16 scaling */
uint64_t slope = numerator / denominator_raw;
return slope;
}
/* ============================================================================
* Phase 2E: Fit Quality Assessment
* ============================================================================
*
* Purpose: Compute R² or residual metric for diagnostics
* Simplified: Use residual sum of squares / total sum of squares
*/
/**
* @brief Compute R²-style fit quality for the inferred decay slope.
*
* Currently returns a fixed placeholder of 0.8 in Q48.16 (52429). A full
* residual-sum-of-squares implementation is planned but deferred — the
* placeholder is sufficient for the DoE convergence signal.
*
* @param heat_data Array of heat values (unused in current implementation)
* @param length Array length (unused in current implementation)
* @param slope_q48 Inferred slope in Q48.16 (unused in current implementation)
* @return Fit quality in Q48.16; currently always 0.8 (52429)
*/
static uint64_t compute_fit_quality(
const uint64_t *heat_data,
uint64_t length,
uint64_t slope_q48
)
{
(void)heat_data;
(void)slope_q48;
if (length < 2) {
return q48_from_u64(1); /* Perfect fit if no data */
}
/* Simplified: Return ratio of predicted-to-actual variance */
/* For now: return 0.8 in Q48.16 as placeholder */
return (q48_16_t)52429ULL; /* 0.8 * 65536 ≈ 52429 in Q48.16 */
}
/* ============================================================================
* Main API: inference_engine_run()
* ============================================================================
*
* High-level orchestrator that coordinates all inference phases
*/
/*
* @brief Run the full inference engine pipeline.
*
* High-level orchestrator for Loops #5 and #6. In order:
* 1. Extract the heat trajectory from the rolling window (Phase 2B).
* 2. ANOVA early-exit check — if variance is stable, skip and set
* @c outputs->early_exited = 1 (Phase 2A).
* 3. Window width inference via Levene's test — gated by L5 flag in
* @c vm->ssm_config (Phase 2C).
* 4. Decay slope inference via log-linear regression — gated by L6 flag
* (Phase 2D).
* 5. Fit quality assessment (Phase 2E).
* Writes all results into @c outputs and frees the trajectory. No-op if
* any required pointer is NULL.
*
* @param inputs Input pointers: rolling window and VM reference
* @param outputs Output struct updated with window, slope, variance, fit quality
*/
void inference_engine_run(InferenceInputs *inputs, InferenceOutputs *outputs)
{
if (!inputs || !outputs || !inputs->window || !inputs->vm) {
return;
}
/* === PHASE 2B: Extract Fresh Heat Trajectory === */
uint64_t traj_len = 0;
uint64_t *trajectory = extract_heat_trajectory(inputs->window, inputs->vm, &traj_len);
if (!trajectory || traj_len < 2) {
outputs->early_exited = 1;
if (trajectory) free(trajectory);
return;
}
/* === PHASE 2A: ANOVA Early-Exit Check (using actual heat samples) === */
q48_16_t current_variance = compute_variance_q48(trajectory, traj_len);
if (has_variance_stabilized(current_variance, outputs->window_variance_q48)) {
outputs->early_exited = 1;
free(trajectory);
return;
}
/* === PHASE 2C: Window Width Inference === */
/* L8 FINAL INTEGRATION: Loop #5 runtime-gated by Jacquard mode selector */
uint32_t inferred_width = outputs->adaptive_window_width; /* Keep previous value as default */
if (inputs->vm && inputs->vm->ssm_config) {
ssm_config_t *config = (ssm_config_t*)inputs->vm->ssm_config;
if (config->L5_window_inference) {
/* L5 enabled - run inference */
inferred_width = find_variance_inflection(
trajectory,
traj_len,
current_variance
);
}
/* else: L5 disabled, keep previous adaptive_window_width */
} else {
/* No L8 config available - run inference (legacy mode) */
inferred_width = find_variance_inflection(
trajectory,
traj_len,
current_variance
);
}
/* === PHASE 2D: Decay Slope Inference === */
/* L8 FINAL INTEGRATION: Loop #6 runtime-gated by Jacquard mode selector */
uint64_t inferred_slope = outputs->adaptive_decay_slope; /* Keep previous value as default */
if (inputs->vm && inputs->vm->ssm_config) {
ssm_config_t *config = (ssm_config_t*)inputs->vm->ssm_config;
if (config->L6_decay_inference) {
/* L6 enabled - run inference */
inferred_slope = infer_decay_slope_q48(trajectory, traj_len);
}
/* else: L6 disabled, keep previous adaptive_decay_slope */
} else {
/* No L8 config available - run inference (legacy mode) */
inferred_slope = infer_decay_slope_q48(trajectory, traj_len);
}
/* === PHASE 2E: Diagnostics === */
uint64_t fit_quality = compute_fit_quality(trajectory, traj_len, inferred_slope);
/* === Update Outputs === */
outputs->adaptive_window_width = inferred_width;
outputs->adaptive_decay_slope = inferred_slope;
outputs->window_variance_q48 = current_variance;
outputs->slope_fit_quality_q48 = fit_quality;
outputs->early_exited = 0;
/* === Cleanup === */
free(trajectory);
}
/* ============================================================================
* Helper Functions: Logging & Validation
* ============================================================================
*/
/**
* @brief Format inference outputs as a human-readable string.
*
* Writes into a static 256-byte buffer — not thread-safe, for diagnostics only.
* Format: @c "window=N var=F slope=F quality=F (cached|full)".
*
* @param outputs Inference outputs to format (NULL produces @c "(null)")
* @return Pointer to a static string buffer
*/
const char* inference_outputs_to_string(const InferenceOutputs *outputs)
{
static char buf[256];
if (!outputs) {
snprintf(buf, sizeof(buf), "(null)");
return buf;
}
double var_dbl = q48_to_double(outputs->window_variance_q48);
double slope_dbl = q48_to_double(outputs->adaptive_decay_slope);
double quality_dbl = q48_to_double(outputs->slope_fit_quality_q48);
snprintf(buf, sizeof(buf),
"window=%u var=%.6f slope=%.6f quality=%.6f %s",
outputs->adaptive_window_width,
var_dbl,
slope_dbl,
quality_dbl,
outputs->early_exited ? "(cached)" : "(full)");
return buf;
}
/**
* @brief Validate inference outputs for sanity.
*
* Checks that @c adaptive_window_width is in
* [@c ADAPTIVE_MIN_WINDOW_SIZE, @c ROLLING_WINDOW_SIZE], that
* @c adaptive_decay_slope is non-zero and <= 100.0 in Q48.16, and that
* @c slope_fit_quality_q48 is in [0.0, 1.0].
*
* @param outputs Inference outputs to validate (NULL returns 0)
* @return 1 if all fields are within acceptable ranges, 0 otherwise
*/
int inference_outputs_validate(const InferenceOutputs *outputs)
{
if (!outputs) {
return 0;
}
/* Check window width is reasonable */
#ifndef ADAPTIVE_MIN_WINDOW_SIZE
#define ADAPTIVE_MIN_WINDOW_SIZE 256
#endif
#ifndef ROLLING_WINDOW_SIZE
#define ROLLING_WINDOW_SIZE 4096
#endif
if (outputs->adaptive_window_width < ADAPTIVE_MIN_WINDOW_SIZE ||
outputs->adaptive_window_width > ROLLING_WINDOW_SIZE) {
return 0;
}
/* Check slope is positive and reasonable */
/* Typical range: 0.001 to 100.0 in Q48.16 */
if (outputs->adaptive_decay_slope == 0 ||
outputs->adaptive_decay_slope > q48_from_u64(100)) {
return 0;
}
/* Check fit quality is between 0.0 and 1.0 */
if (outputs->slope_fit_quality_q48 > q48_from_u64(1)) {
return 0;
}
return 1;
}
+104
View File
@@ -0,0 +1,104 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "io.h"
#include "log.h"
#include <string.h>
/** @} */
/**
* @brief Initialize the IO subsystem
* @param vm Pointer to VM instance
*
* Initializes the block device view over the unified VM memory.
* Zeros out the entire VM memory space.
*/
void io_init(VM *vm) {
if (!vm || !vm->memory) {
log_message(LOG_ERROR, "io_init: VM or VM memory not initialized");
return;
}
/* Zero the entire VM address space so blocks start clean. */
memset(vm->memory, 0, VM_MEMORY_SIZE);
log_message(LOG_INFO,
"IO subsystem initialized with %d blocks of %d bytes",
(int) MAX_BLOCKS, (int) BLOCK_SIZE);
}
/*
* @brief Write a block to VM memory
* @param vm Pointer to VM instance
* @param block_num Block number to write (0 to MAX_BLOCKS-1)
* @param buffer Source buffer containing block data
* @return 0 on success, -1 on error
*/
/**
* @brief Write a block to VM memory
* @param vm Pointer to VM instance
* @param block_num Block number to write (0 to MAX_BLOCKS-1)
* @param buffer Source buffer containing block data
* @return 0 on success, -1 on error
*
* Writes a BLOCK_SIZE chunk of data from the provided buffer into the VM's memory
* at the specified block number offset. The block number must be within valid range.
* The entire block is written atomically.
*
* @note The buffer must contain at least BLOCK_SIZE bytes of valid data
*/
int io_write_block(VM *vm, int block_num, const unsigned char *buffer) {
if (!vm || !vm->memory || !buffer) {
log_message(LOG_ERROR, "io_write_block: bad args");
return -1;
}
if (block_num < 0 || block_num >= (int) MAX_BLOCKS) {
log_message(LOG_ERROR, "io_write_block: invalid block number %d", block_num);
return -1;
}
size_t off = (size_t) block_num * (size_t) BLOCK_SIZE;
memcpy(&vm->memory[off], buffer, BLOCK_SIZE);
log_message(LOG_DEBUG, "io_write_block: wrote block %d", block_num);
return 0;
}
+198
View File
@@ -0,0 +1,198 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/log.h"
#include "../include/platform_time.h"
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
/* Current log level (default to LOG_INFO) */
static LogLevel current_level = LOG_INFO;
/* Log level names */
static const char *level_names[] = {
"ERROR",
"WARN",
"INFO",
"TEST",
"DEBUG"
};
/* ANSI color codes for each log level - only used outside minimal mode;
* MINIMAL builds never read these (both use sites are #ifndef-guarded
* below), so no empty-string fallback definitions are needed. */
#ifndef STARFORTH_MINIMAL
static const char *level_colors[] = {
"\x1b[31m", // ERROR - Red
"\x1b[33m", // WARN - Yellow
"\x1b[32m", // INFO - Green
"\x1b[35m", // TEST - Magenta
"\x1b[34m" // DEBUG - Blue
};
/* Test result colors */
static const char *test_pass_color = "\x1b[32m"; // Green
static const char *test_fail_color = "\x1b[31m"; // Red
static const char *test_skip_color = "\x1b[33m"; // Yellow
/* Reset color */
static const char *color_reset = "\x1b[0m";
#endif
/* Set global log level */
/*
* @brief Sets the global logging level
*
* @param level The LogLevel to set as current logging level
*/
void log_set_level(LogLevel level) {
current_level = level;
}
/* Get current log level */
/**
* @brief Gets the current global logging level
*
* @return LogLevel Current logging level
*/
LogLevel log_get_level(void) {
return current_level;
}
/* Timestamp helper */
/**
* @brief Helper function to generate timestamp string
*
* @param buffer Output buffer for the timestamp string
* @param size Size of the output buffer
*/
static void get_timestamp(char *buffer, size_t size) {
/* Use platform abstraction for timestamps */
sf_time_ns_t now = sf_realtime_ns();
sf_format_timestamp(now, buffer, 1); /* 24-hour format */
}
/* Generic logger */
/*
* @brief Logs a message with the specified level and format
*
* @param level The LogLevel for this message
* @param fmt Printf-style format string
* @param ... Variable arguments for format string
*/
void log_message(LogLevel level, const char *fmt, ...) {
/* LOG_NONE (-1) disables ALL logging */
if (current_level == LOG_NONE || level > current_level)
return;
char timestamp[16];
get_timestamp(timestamp, sizeof(timestamp));
#ifndef STARFORTH_MINIMAL
fprintf(stderr, "%s[%s] %s: %s", level_colors[level], timestamp, level_names[level], color_reset);
#else
fprintf(stderr, "[%s] %s: ", timestamp, level_names[level]);
#endif
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, "\n");
}
/* Dedicated test result logger (only shown for LOG_TEST or higher) */
/*
* @brief Logs test results_run_01_2025_12_08 with appropriate formatting and colors
*
* @param word_name Name of the word being tested
* @param result Test result (PASS/FAIL/SKIP)
*/
void log_test_result(const char *word_name, TestResult result) {
if (current_level == LOG_NONE || current_level < LOG_TEST)
return;
char timestamp[16];
get_timestamp(timestamp, sizeof(timestamp));
#ifndef STARFORTH_MINIMAL
const char *color;
#endif
const char *status;
switch (result) {
case TEST_PASS:
#ifndef STARFORTH_MINIMAL
color = test_pass_color;
#endif
status = "PASS";
break;
case TEST_FAIL:
#ifndef STARFORTH_MINIMAL
color = test_fail_color;
#endif
status = "FAIL";
break;
case TEST_SKIP:
#ifndef STARFORTH_MINIMAL
color = test_skip_color;
#endif
status = "SKIP";
break;
default:
#ifndef STARFORTH_MINIMAL
color = color_reset;
#endif
status = "????";
break;
}
#ifndef STARFORTH_MINIMAL
fprintf(stderr, "\x1b[35m[%s] TEST: %sTesting %-12s ... %s%s%s\n",
timestamp, color_reset, word_name, color, status, color_reset);
#else
fprintf(stderr, "[%s] TEST: Testing %-12s ... %s\n",
timestamp, word_name, status);
#endif
}
+563
View File
@@ -0,0 +1,563 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "vm.h"
#include "vm_debug.h"
#include "log.h"
#include "profiler.h"
#include "physics_runtime.h"
#include "platform_time.h"
#include "version.h"
#include "cli.h"
#include "block_subsystem.h"
#include "test_runner/include/test_runner.h"
#include "test_runner/include/test_common.h" /* brings in extern int fail_fast; */
#include "doe_metrics.h"
#include "blkio.h"
#include "blkio_factory.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <ctype.h>
#include <stdint.h>
#ifdef __unix__
#include <sys/mman.h>
#include <unistd.h>
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
#endif
#ifndef BLKIO_FORTH_BLOCK_SIZE
#define BLKIO_FORTH_BLOCK_SIZE 1024u
#endif
#ifndef STARFORTH_STATE_BYTES
#define STARFORTH_STATE_BYTES 8192u
#endif
/* Optional hook (weak): if your block layer exposes an attach function, we call it. */
#if defined(__GNUC__) || defined(__clang__)
__attribute__ ((weak))
#endif
void blk_layer_attach_device(blkio_dev_t * dev);
/* Global VM pointer for cleanup */
static VM* global_vm = NULL;
/* Global blkio device + RAM backing (for cleanup) */
static blkio_dev_t g_blkio = {0};
static int g_blkio_opened = 0;
static uint8_t* g_ram_base = NULL;
static size_t g_ram_bytes = 0;
static int g_ram_is_mmap = 0;
/* Cleanup function for atexit() and signal handlers */
static void cleanup_blkio(void)
{
if (g_blkio_opened)
{
blkio_flush(&g_blkio);
blkio_close(&g_blkio);
g_blkio_opened = 0;
}
#ifdef __unix__
if (g_ram_base)
{
if (g_ram_is_mmap) munmap(g_ram_base, g_ram_bytes);
else free(g_ram_base);
g_ram_base = NULL;
g_ram_bytes = 0;
g_ram_is_mmap = 0;
}
#else
if (g_ram_base)
{
free(g_ram_base);
g_ram_base = NULL;
g_ram_bytes = 0;
}
#endif
}
/**
* @brief Performs cleanup operations before program termination
*/
void cleanup_and_exit(void)
{
/* close blkio & free RAM before VM */
cleanup_blkio();
physics_runtime_shutdown();
if (global_vm != NULL)
{
vm_cleanup(global_vm);
global_vm = NULL;
}
}
/* Signal handler for CTRL-C (SIGINT) and SIGTERM */
void signal_handler(int sig)
{
switch (sig)
{
case SIGINT: printf("\nInterrupted! Cleaning up...\n");
break;
case SIGTERM: printf("\nTerminating! Cleaning up...\n");
break;
default: break;
}
cleanup_and_exit();
exit(0);
}
/**
* @brief Prints program usage information
* @param program_name Name of the executable
*/
void print_usage(const char* program_name)
{
printf("Usage: %s [OPTIONS]\n", program_name);
printf("StarForth - A lightweight Forth virtual machine\n\n");
printf("Options:\n");
printf(" --stress-tests Run stress tests (deep nesting, stack exhaustion, large definitions)\n");
printf(" --integration Run integration tests (complete Forth programs)\n");
printf(" --break-me 🔥 ULTRA-COMPREHENSIVE diagnostic mode - tests EVERYTHING,\n");
printf(" generates detailed markdown report, includes easter egg surprise!\n");
printf(" --benchmark [N] Run performance benchmarks (default: 1000 iterations)\n");
printf(" (exits after benchmarking, does not start REPL)\n");
printf(" --log-error Set logging level to ERROR (only errors)\n");
printf(" --log-warn Set logging level to WARN (warnings and errors)\n");
printf(" --log-info Set logging level to INFO (default)\n");
printf(" --log-test Set logging level to TEST (test results only)\n");
printf(" --log-debug Set logging level to DEBUG (all messages)\n");
printf(" --log-none Disable all logging (maximum performance)\n");
printf(" --fail-fast Stop test suite immediately on first failure\n");
printf(" --profile [0-3] Enable profiler: 1=basic, 2=detailed, 3=full\n");
printf(" --profile-report Emit profiler report on exit\n");
printf(" --disk-img=<path> Use raw disk image file at <path>\n");
printf(" --disk-ro Open disk image read-only\n");
printf(" --ram-disk=<MB> RAM fallback size if no --disk-img (default: 1 MB)\n");
printf(" --fbs=<bytes> Forth block size (default: 1024)\n");
printf(" --version, -v Show version information and exit\n");
printf(" --help, -h Show this help message\n\n");
printf("Tests are executed automatically before the REPL starts.\n\n");
printf("Examples:\n");
printf(" %s # Start REPL with INFO logging\n", program_name);
printf(" %s --benchmark # Run benchmarks with 1000 iterations\n", program_name);
printf(" %s --benchmark 5000 # Run benchmarks with 5000 iterations\n", program_name);
printf(" %s --disk-img=./disks/starship.img # Use disk image\n", program_name);
printf(" %s --ram-disk=64 --fbs=1024 # 64 MB RAM fallback\n", program_name);
}
/* ============================================================================
* STATIC HELPER FUNCTIONS
* ============================================================================ */
/**
* @brief Initialize block I/O from CLI configuration
*/
static int init_blkio(const CLIConfig* config)
{
const int requested_file = (config->disk_img_path && *config->disk_img_path) ? 1 : 0;
int using_file = requested_file;
/* Verify file accessibility if requested */
if (requested_file)
{
FILE* chk = fopen(config->disk_img_path, "rb");
if (!chk)
{
log_message(LOG_WARN,
"Disk image '%s' not found or not accessible; falling back to RAM disk",
config->disk_img_path);
using_file = 0;
}
else
{
fclose(chk);
}
}
/* Compute RAM geometry (FBS fixed at 1024 bytes) */
uint64_t ram_bytes_u64 = (uint64_t)config->ram_disk_mb * 1024ull * 1024ull;
uint32_t fbs = BLKIO_FORTH_BLOCK_SIZE;
uint32_t ram_blocks = (uint32_t)(ram_bytes_u64 / (uint64_t)fbs);
g_ram_bytes = (size_t)ram_blocks * (size_t)fbs;
/* Allocate RAM backing if not using file */
if (!using_file)
{
if (g_ram_bytes == 0)
{
g_ram_bytes = (size_t)fbs;
}
#ifdef __unix__
void* p = mmap(NULL, g_ram_bytes, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS
#ifdef MAP_NORESERVE
| MAP_NORESERVE
#endif
, -1, 0);
if (p != MAP_FAILED)
{
g_ram_is_mmap = 1;
g_ram_base = (uint8_t*)p;
}
#endif
if (!g_ram_base)
{
g_ram_base = (uint8_t*)malloc(g_ram_bytes);
if (!g_ram_base)
{
fprintf(stderr, "Failed to allocate RAM-disk (%zu bytes)\n", g_ram_bytes);
return 1;
}
}
}
/* Open block device */
static uint8_t state_buf[STARFORTH_STATE_BYTES];
uint8_t used_file = 0;
const uint32_t total_blocks_hint = 0;
int rc = blkio_factory_open(&g_blkio,
using_file ? config->disk_img_path : NULL,
0, /* disk_read_only: always false */
total_blocks_hint,
fbs,
state_buf, sizeof(state_buf),
g_ram_base,
(uint32_t)(g_ram_bytes / (size_t)fbs),
&used_file);
if (rc != BLKIO_OK)
{
if (using_file)
fprintf(stderr, "Failed to open disk image '%s' (rc=%d)\n", config->disk_img_path, rc);
else
fprintf(stderr, "Failed to initialize RAM backend (rc=%d)\n", rc);
return 1;
}
g_blkio_opened = 1;
/* Log geometry */
blkio_info_t info = {0};
if (blkio_info(&g_blkio, &info) == BLKIO_OK)
{
log_message(LOG_INFO,
"blkio: backend=%s fbs=%u blocks=%u size=%lluB ro=%u",
used_file ? "FILE" : "RAM",
(unsigned)info.forth_block_size,
(unsigned)info.total_blocks,
(unsigned long long)info.phys_size_bytes,
(unsigned)info.read_only);
}
return 0;
}
/**
* @brief Initialize VM core and block subsystem
*/
static int init_vm_and_subsystem(VM* vm)
{
/* Initialize VM */
vm_init(vm);
setvbuf(stdout, NULL, _IOFBF, 1 << 16);
if (vm->error)
{
fprintf(stderr, "Failed to initialize VM\n");
return 1;
}
/* Initialize block subsystem */
extern int blk_subsys_init(VM* vm, uint8_t* ram_base, size_t ram_size);
static uint8_t blk_ram[BLK_RAM_BLOCKS * BLK_FORTH_SIZE];
int rc = blk_subsys_init(vm, blk_ram, sizeof(blk_ram));
if (rc != 0)
{
fprintf(stderr, "Failed to initialize block subsystem (rc=%d)\n", rc);
return 1;
}
/* Attach blkio device */
if (g_blkio_opened && blk_layer_attach_device)
{
blk_layer_attach_device(&g_blkio);
}
return 0;
}
/**
* @brief Run benchmark mode and exit
*/
static void run_benchmark_mode(VM* vm, const CLIConfig* config)
{
log_message(LOG_INFO, "==============================================");
log_message(LOG_INFO, " StarForth Performance Benchmark Suite");
log_message(LOG_INFO, "==============================================\n");
/* run_compute_benchmarks(vm); */
log_message(LOG_INFO, "\n==============================================");
log_message(LOG_INFO, " Full Test Suite Benchmark");
log_message(LOG_INFO, " (Running %d iterations per module)", config->benchmark_iterations);
log_message(LOG_INFO, "==============================================\n");
enable_benchmark_mode(config->benchmark_iterations);
run_all_tests(vm);
}
/**
* @brief Run optional test modes after system initialization
*/
static void run_optional_tests(VM* vm, const CLIConfig* config)
{
if (config->break_me)
{
run_break_me_tests(vm);
}
}
/**
* @brief Run Design of Experiments experiment
*
* Executes the full test harness N times, collecting metrics after each run.
* Writes results to CSV file for statistical analysis.
*/
static void run_doe_experiment(VM* vm)
{
/* Reset statistics for clean start */
vm_interpret(vm, "PHYSICS-RESET-STATS");
/* Capture CPU state before run */
/* HISTORICAL: this TODO asked about l4re/POSIX separation of concern
* for these metrics -- moot now that L4Re has been removed as an
* active target; POSIX is the only path. */
int32_t cpu_temp_before = metrics_get_cpu_temp_c();
int32_t cpu_freq_before = metrics_get_cpu_freq_mhz();
/* Capture monotonic time before workload */
sf_time_ns_t workload_start_ns = sf_monotonic_ns();
/* Run test harness once (all 936+ tests) */
run_all_tests(vm);
/* Capture monotonic time after workload */
sf_time_ns_t workload_end_ns = sf_monotonic_ns();
/* Calculate actual VM workload execution time in nanoseconds */
uint64_t workload_duration_ns = workload_end_ns - workload_start_ns;
/* Capture CPU state after run */
int32_t cpu_temp_after = metrics_get_cpu_temp_c();
int32_t cpu_freq_after = metrics_get_cpu_freq_mhz();
/* Calculate deltas and convert to Q48.16 */
int32_t cpu_temp_delta = cpu_temp_after - cpu_temp_before;
int32_t cpu_freq_delta = cpu_freq_after - cpu_freq_before;
/* DEBUG: Log final rolling window state before metrics extraction */
log_message(LOG_INFO, "DoE FINAL STATE: rolling_window.total_executions=%lu, is_warm=%d, effective_window_size=%u",
vm->rolling_window.total_executions,
vm->rolling_window.is_warm,
vm->rolling_window.effective_window_size);
/* Extract metrics from VM with deltas */
DoeMetrics metrics = metrics_from_vm(vm, workload_duration_ns,
cpu_temp_delta, cpu_freq_delta);
/* CSV output suppressed - metrics collected but not printed to stdout.
* Rationale: DoE experiments now rely on internal VM metrics and test output only.
* The CSV row is redundant and interferes with clean test harness output.
* If CSV export is needed in the future, consider writing to a file instead
* of stdout, or add a --csv-export flag. */
/* metrics_write_csv_row(stdout, &metrics); */
(void)metrics; /* Suppress unused variable warning */
}
/* ============================================================================
* MAIN ENTRY POINT
* ============================================================================ */
/**
* @brief Main entry point for StarForth
*/
int main(int argc, char* argv[])
{
/* Parse command line arguments */
CLIConfig config = cli_parse(argc, argv);
/* Initialize platform time subsystem FIRST */
sf_time_init();
/* Register cleanup function and signal handlers */
atexit(cleanup_and_exit);
signal(SIGINT, signal_handler); /* CTRL-C */
signal(SIGTERM, signal_handler); /* Termination */
/* DoE mode: force LOG_NONE immediately to suppress all output except CSV */
if (config.doe_experiment)
{
config.log_level = LOG_NONE;
}
/* Initialize logging and profiling */
if (config.run_tests_flag_observed && !config.log_level_explicitly_set && !config.benchmark && !config.doe_experiment)
{
config.log_level = LOG_TEST;
}
log_set_level(config.log_level);
if (physics_runtime_init(0) != 0)
{
log_message(LOG_WARN, "Physics runtime failed to allocate analytics heap; proceeding degraded");
}
if (config.run_tests_flag_observed)
{
log_message(LOG_WARN, "--run-tests flag is obsolete; tests now run automatically.");
if (!config.log_level_explicitly_set && !config.benchmark)
{
log_message(LOG_INFO, "Test mode enabled - using LOG_TEST level for diagnostics");
}
}
/* Initialize block I/O device */
if (init_blkio(&config) != 0)
{
cleanup_and_exit();
return 1;
}
/* Initialize VM and block subsystem */
VM vm = {0};
global_vm = &vm;
if (init_vm_and_subsystem(&vm) != 0)
{
cleanup_and_exit();
return 1;
}
/* Register VM lifecycle words (BIRTH KILL PAUSE RESUME USE) */
extern void register_lifecycle_words(VM *vm);
register_lifecycle_words(&vm);
/* Install VM debug hooks */
vm_debug_set_current_vm(&vm);
vm_debug_install_signal_handlers();
/* Handle DoE experiment mode (exits after) */
if (config.doe_experiment)
{
/* Force LOG_NONE so only CSV goes to stdout */
log_set_level(LOG_NONE);
/* Run DoE experiment - returns non-zero on runtime failure */
run_doe_experiment(&vm);
/* Check for VM errors (runtime instability) */
if (vm.error)
{
cleanup_and_exit();
return 2; /* Runtime failure */
}
cleanup_and_exit();
return 0; /* Success */
}
/* Handle benchmark mode (exits after) */
if (config.benchmark)
{
run_benchmark_mode(&vm, &config);
cleanup_and_exit();
return 0;
}
/* Apply global fail-fast flag to test framework */
if (config.fail_fast)
{
fail_fast = 1;
}
/* Protect foundational words from FORGET */
vm.dict_fence_latest = vm.latest;
vm.dict_fence_here = vm.here;
log_message(LOG_INFO, "Dictionary fence set - init words protected from FORGET");
/* Run optional test modes */
run_optional_tests(&vm, &config);
/* Start REPL unless --break-me was specified */
if (!config.break_me)
{
/* Always run the full test suite immediately prior to REPL */
log_message(LOG_INFO, "Running comprehensive test suite...");
run_all_tests(&vm);
log_message(LOG_INFO, "Test run complete.");
/* Run system initialization AFTER POST completes */
log_message(LOG_INFO, "Running system initialization (INIT)...");
vm_interpret(&vm, "INIT");
if (vm.error)
{
log_message(LOG_ERROR, "System initialization failed - cannot continue");
cleanup_and_exit();
return 1;
}
log_message(LOG_INFO, "System initialization complete.");
vm_repl(&vm, config.script_mode);
}
cleanup_and_exit();
return 0;
}
+263
View File
@@ -0,0 +1,263 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* math_portable.c
*
* Pure 64-bit fixed-point Q48.16 math functions.
* Suitable for embedded environments with no libm or floating-point
* (HISTORICAL: also L4Re, through mid-2026; removed as an active target).
* All arithmetic is integer-only for maximum production performance.
*
* Q48.16 Format:
* - 48-bit signed integer + 16-bit fractional part
* - 1.0 = 65536 (2^16)
* - Precision: 2^-16 ≈ 0.0000153
* - Range: ±140 trillion
*/
#include <stdint.h>
#include "math_portable.h"
/**
* @brief Compute exp(@p x) via Taylor series in Q48.16 fixed-point.
*
* Evaluates the classic Taylor expansion:
* @code
* exp(x) = 1 + x + x²/2! + x³/3! + … (up to 40 terms)
* @endcode
* entirely in 64-bit integer arithmetic using the Q48.16 representation
* (1.0 = 65536 = 2^16).
*
* The function is intentionally scoped to the range the physics engine
* actually uses — the argument to @c erf_q48() is @c -x² times a ratio
* near 1, yielding values in roughly the @c [-20, 0] interval. Outside
* this range the implementation provides coarse-grained clamping:
* - @p x == 0 → returns @c Q48 (1.0 exactly)
* - @p x > 20.0 → saturates at @c Q48 << 8 (approx e^20, avoids overflow)
* - @p x < -100.0 → underflows to 0 (result is negligible)
*
* The term loop exits early on the first term smaller than 1 ULP (the
* series has converged) and guards against intermediate overflow by
* clamping @c result at @c INT64_MAX-1.
*
* @note This is a file-local (@c static) helper; callers are
* @c erf_q48() only.
*
* @param x Input value in Q48.16 format (1.0 = 65536). Intended range:
* approximately [-100×65536, +20×65536].
* @return exp(@p x) in Q48.16 format; 0 on underflow; saturated at
* @c INT64_MAX-1 on overflow.
*/
static int64_t exp_q48(int64_t x) {
const int64_t Q48 = 65536LL; /* 2^16 */
/* Handle extremes */
if (x == 0) return Q48;
if (x > (20LL << 16)) return Q48 << 8; /* Approximate e^20 */
if (x < -(100LL << 16)) return 0; /* Underflow to zero */
/* Taylor series: exp(x) = Σ x^n / n! (in fixed-point) */
int64_t result = Q48; /* Start with 1 */
int64_t term = Q48;
int64_t x_power = x; /* x^1 */
for (int i = 1; i < 40; i++) {
/* term = x^i / i! */
term = (x_power / (int64_t)i) / Q48; /* Scale down to avoid overflow */
if (term > -1 && term < 1) break; /* Negligible */
result += term;
x_power = (x_power * x) / Q48; /* Keep x^n scaled */
/* Guard against overflow */
if (result < 0 && result < (1LL << 62)) break;
if (result > 0 && result > (1LL << 62)) {
result = 9223372036854775806LL; /* Saturate at INT64_MAX-1 */
break;
}
}
return result;
}
/**
* @brief Compute sqrt(@p x) via Newton-Raphson in Q48.16 fixed-point.
*
* Applies the Newton-Raphson recurrence:
* @code
* r_{n+1} = (r_n² + x) / (2 × r_n)
* @endcode
* for up to 20 iterations, with all arithmetic scaled to the Q48.16
* representation (1.0 = 65536). Intermediate products use 8-bit right
* shifts on both operands before squaring to prevent 64-bit overflow:
* @code
* guess_squared = (guess >> 8) * (guess >> 8)
* @endcode
* This sacrifices 16 bits of precision in the squared term, which is
* acceptable because the division by @c 2*guess restores the correct
* order of magnitude. Convergence is declared when successive iterates
* differ by at most 1 in Q48.16 units (sub-nanosecond for timing uses).
*
* Special cases handled without iteration:
* - @p x ≤ 0 → returns 0 (no imaginary results)
* - @p x == 65536 (1.0) → returns 65536 exactly
*
* Initial guess: @c x/2 for @p x > 1.0, else @c 1.0. Values above 1.0
* converge from above; values in (0, 1.0) converge from the 1.0 initial
* guess.
*
* @param x Non-negative value in Q48.16 format (1.0 = 65536).
* Negative values are treated as zero.
* @return sqrt(@p x) in Q48.16 format; 0 for @p x ≤ 0.
*/
int64_t sqrt_q48(int64_t x) {
const int64_t Q48 = 65536LL;
/* Handle special cases */
if (x < 0) return 0;
if (x == 0) return 0;
if (x == Q48) return Q48; /* sqrt(1.0) = 1.0 */
/* Initial guess: average of x and 1 */
int64_t guess = (x > Q48) ? (x >> 1) : Q48;
/* Newton-Raphson iterations */
for (int i = 0; i < 20; i++) {
/* next = (guess^2 + x) / (2 * guess)
* In Q48.16: (guess^2) >> 16 + x, then divide by 2*guess
*/
int64_t guess_squared = (guess >> 8) * (guess >> 8); /* Avoid overflow */
int64_t next = (guess_squared + (x >> 16)) / (2 * guess);
/* Check convergence */
int64_t diff = next - guess;
if (diff > -2 && diff < 2) return next;
guess = next;
}
return guess;
}
/**
* @brief Compute erf(@p x) via Abramowitz & Stegun 7.1.26 in Q48.16 fixed-point.
*
* Implements the rational approximation from Abramowitz & Stegun, formula 7.1.26:
* @code
* erf(x) ≈ sign(x) × sqrt(1 − exp(−x² × (4/π + a·x²) / (1 + a·x²)))
* @endcode
* where @c a ≈ 0.147 (in Q48.16: 9633). The maximum error of the true
* mathematical approximation is @c |ε| < 1.5×10⁻⁷; the fixed-point
* encoding introduces additional rounding of approximately 1 ULP in Q48.16.
*
* All arithmetic is performed in 64-bit integer Q48.16 format — no
* floating-point operations. The computation sequence:
* 1. Extract sign; work with @c |x|.
* 2. Compute @c x² with 8-bit prescaling to prevent overflow:
* @c x_sq = (x >> 8) * (x >> 8).
* 3. Evaluate the A&S numerator @c (4/π + a·x²) and denominator @c (1 + a·x²).
* 4. Compute @c ratio = numerator / denominator (shift to preserve Q48.16 scale).
* 5. Compute @c exp_arg = −x² × ratio and evaluate @c exp_q48(exp_arg).
* 6. Compute @c base = 1 − exp_val, clamp to [0, 1].
* 7. Return @c sign × sqrt_q48(base).
*
* Q48.16 constants used:
* - @c a_q48 = 9633 (0.147 × 65536)
* - @c four_over_pi_q48 = 83328 (4/π × 65536 ≈ 1.2732 × 65536)
*
* Consumed by @c hotwords_speedup_estimate() to compute P(speedup > 1.1×)
* and P(speedup > 2.0×) via the normal CDF identity:
* @code
* P(Z ≤ z) = 0.5 × (1 + erf(z / √2))
* @endcode
* where @c √2 in Q48.16 = 92681.
*
* @param x Input value in Q48.16 format (dimensionless Z-score;
* 1.0 = 65536). May be negative; sign determines the sign
* of the result.
* @return erf(@p x) in Q48.16 format, in the range [−65536, +65536]
* corresponding to (−1.0, +1.0).
*/
int64_t erf_q48(int64_t x) {
const int64_t Q48 = 65536LL;
if (x == 0) return 0;
/* Compute sign and work with absolute value */
int sign = (x < 0) ? -1 : 1;
if (x < 0) x = -x;
/* Abramowitz & Stegun coefficient a ≈ 0.147 (in Q48.16) */
const int64_t a_q48 = 9633LL; /* 0.147 * 65536 */
const int64_t four_over_pi_q48 = 83328LL; /* 4/π * 65536 */
/* Compute x^2 in Q48.16 */
int64_t x_sq = (x >> 8) * (x >> 8); /* Scaled to avoid overflow */
/* Compute denominator: 1 + a*x^2 */
int64_t denom = Q48 + ((a_q48 * x_sq) >> 16);
if (denom == 0) denom = 1; /* Guard divide-by-zero */
/* Compute numerator: 4/π + a*x^2 */
int64_t numer = four_over_pi_q48 + ((a_q48 * x_sq) >> 16);
/* Compute ratio = numerator / denominator */
int64_t ratio = (numer << 16) / denom;
/* Compute exp argument: -x^2 * ratio */
int64_t exp_arg = -(x_sq * ratio / Q48);
/* Compute exp(-x^2 * ratio) */
int64_t exp_val = exp_q48(exp_arg);
/* Compute base: 1 - exp(exp_arg) */
int64_t base = Q48 - exp_val;
if (base < 0) base = 0;
if (base > Q48) base = Q48;
/* Compute sqrt(base) */
int64_t sqrt_base = sqrt_q48(base);
/* Apply sign and return */
return sign * sqrt_base;
}
+140
View File
@@ -0,0 +1,140 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/vm.h"
#include "../include/log.h"
/**
* @brief Allocates memory from the VM's dictionary space
* @param vm Pointer to the VM instance
* @param bytes Number of bytes to allocate
* @return Pointer to the allocated memory or NULL if allocation fails
* @note The allocation is done from the VM's dictionary space which is limited to DICTIONARY_MEMORY_SIZE
*/
void *vm_allot(VM *vm, size_t bytes) {
if (!vm || !vm->memory) {
log_message(LOG_ERROR, "vm_allot: VM or memory is NULL");
return NULL;
}
/* Ensure we don't allocate beyond dictionary space (first DICTIONARY_BLOCKS blocks = DICTIONARY_MEMORY_SIZE bytes) */
log_message(LOG_DEBUG, "vm_allot: Requesting %zu bytes, current here=%zu, limit=%d",
bytes, vm->here, DICTIONARY_MEMORY_SIZE);
if (vm->here + bytes >= DICTIONARY_MEMORY_SIZE) {
log_message(LOG_ERROR, "Dictionary space full (here=%zu, bytes=%zu, dict_limit=%d)",
vm->here, bytes, DICTIONARY_MEMORY_SIZE);
vm->error = 1;
return NULL;
}
void *ptr = vm->memory + vm->here;
vm->here += bytes;
/* Log which block we're using */
int current_block = vm->here / BLOCK_SIZE;
log_message(LOG_DEBUG, "ALLOT: Allocated %zu bytes at offset %zu (block %d)",
bytes, vm->here - bytes, current_block);
return ptr;
}
/**
* @brief Aligns the VM's dictionary pointer (here) to the cell boundary
* @param vm Pointer to the VM instance
* @note Adds padding bytes if necessary to ensure proper alignment for cell_t type
*/
void vm_align(VM *vm) {
size_t align = sizeof(cell_t);
size_t misalignment = vm->here % align;
if (misalignment != 0) {
size_t padding = align - misalignment;
void *pad = vm_allot(vm, padding);
if (pad == NULL) {
log_message(LOG_ERROR, "vm_align: Out of memory while aligning");
vm->error = 1;
return;
}
/* Optional: zero out the padding */
unsigned char *bytes = (unsigned char *) pad;
for (size_t i = 0; i < padding; ++i) {
bytes[i] = 0;
}
log_message(LOG_DEBUG, "vm_align: Added %zu byte(s) of padding", padding);
}
}
/**
* @brief Gets the memory address for a specified block number
* @param vm Pointer to the VM instance
* @param block_num Block number to get the address for
* @return Pointer to the start of the block or NULL if block number is invalid
* @note Blocks are fixed-size memory regions of BLOCK_SIZE bytes
*/
void *vm_get_block_addr(VM *vm, int block_num) {
if (block_num < 0 || block_num >= MAX_BLOCKS) {
log_message(LOG_ERROR, "vm_get_block_addr: Invalid block number %d", block_num);
return NULL;
}
return vm->memory + (block_num * BLOCK_SIZE);
}
/* Convert address to block number */
/**
* @brief Converts a memory address to its corresponding block number
* @param vm Pointer to the VM instance
* @param addr Memory address to convert
* @return Block number (0 to MAX_BLOCKS-1) or -1 if address is outside VM memory
* @note Used for determining which block a memory address belongs to
*/
int vm_addr_to_block(VM *vm, void *addr) {
if (addr < (void *) vm->memory ||
addr >= (void *) (vm->memory + VM_MEMORY_SIZE)) {
return -1; /* Outside VM memory */
}
uintptr_t offset = (uintptr_t) addr - (uintptr_t) vm->memory;
return (int) (offset / BLOCK_SIZE);
}
+223
View File
@@ -0,0 +1,223 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* physics_execution_hooks.c - All physics machinery in one place
*
* This file absorbs ALL the physics-related code that was scattered
* throughout execute_colon_word and vm_interpret_word.
*
* The VM execution loop is now clean; this file is where physics lives.
*/
#include "../include/physics_execution_hooks.h"
#include "../include/platform_time.h"
#include "../include/profiler.h"
#include "../include/physics_metadata.h"
#include "../include/physics_hotwords_cache.h"
#include "../include/physics_pipelining_metrics.h"
#include "../include/rolling_window_of_truth.h"
#include "../include/ssm_jacquard.h"
/**
* @brief Pre-execution physics hook — called before every word dispatch.
*
* Consolidates all physics work that must happen before a word runs:
* - Loop #3: applies linear decay based on elapsed time since last execution
* - Loop #1: increments @c execution_heat for the word
* - Loop #2: records the word-id into the rolling window of truth
* - Loop #4: detects a pipelining prefetch hit (if @c prev's speculation
* matched @c word), records the transition from @c prev to @c word,
* updates the probability cache, and issues a speculative hotwords
* cache promotion for the most likely next word
*
* @param vm Pointer to the VM structure
* @param word DictEntry being executed
* @param prev DictEntry executed immediately before @c word (may be NULL)
*/
void physics_pre_execute(VM* vm, DictEntry* word, DictEntry* prev)
{
if (!vm || !word) return;
uint64_t now_ns = sf_monotonic_ns();
/* Loop #3: Apply linear decay before accumulating new heat.
* Tick-based, not wall-clock -- see physics_metadata_apply_linear_decay(). */
uint64_t elapsed_ticks = vm->heartbeat.tick_count - word->physics.last_decay_tick;
physics_metadata_apply_linear_decay(word, elapsed_ticks, vm);
word->physics.last_decay_tick = vm->heartbeat.tick_count;
word->physics.last_active_ns = now_ns;
word->physics.last_decay_ns = now_ns;
/* Loop #1: Heat accumulation */
physics_execution_heat_increment(word);
uint32_t word_id = word->word_id;
if (word_id >= DICTIONARY_SIZE) return;
/* Loop #2: Rolling Window of Truth */
rolling_window_record_execution(&vm->rolling_window, word_id);
/* Loop #4: Pipelining Transition Metrics. ENABLE_PIPELINING is the
* compile-time ceiling; ssm_config's L4_pipelining is L8's live
* runtime toggle within that ceiling (see ssm_jacquard.h). */
{
ssm_config_t *ssm_cfg = (ssm_config_t*)vm->ssm_config;
if (!prev || !prev->transition_metrics || !ENABLE_PIPELINING ||
!ssm_cfg || !ssm_cfg->L4_pipelining) return;
}
/* Check if current word was speculatively promoted by previous word */
uint32_t prev_speculation_target = prev->transition_metrics->most_likely_next_word_id;
if (prev_speculation_target == word_id && prev->transition_metrics->prefetch_attempts > 0)
{
/* PREFETCH HIT: Current word matches previous word's speculation! */
transition_metrics_record_prefetch_hit(prev->transition_metrics, 0);
vm->pipeline_metrics.prefetch_hits++;
}
/* Record transition from previous word to current word */
transition_metrics_record(prev->transition_metrics, word_id, DICTIONARY_SIZE);
/* Update probability cache to find most likely next word */
transition_metrics_update_cache(prev->transition_metrics, DICTIONARY_SIZE);
uint32_t speculated_word_id = prev->transition_metrics->most_likely_next_word_id;
/* Speculative prefetch of most likely next word */
if (speculated_word_id < DICTIONARY_SIZE &&
transition_metrics_should_speculate(prev->transition_metrics, speculated_word_id))
{
DictEntry* spec_entry = vm_dictionary_lookup_by_word_id(vm, speculated_word_id);
/* ENABLE_HOTWORDS_CACHE is the compile-time ceiling; cache->enabled
* is L8's live runtime toggle within that ceiling, kept in sync
* with ssm_config's L1_heat_tracking wherever L8 applies a config
* (see vm_time.c / vm_runtime.c). */
if (spec_entry && vm->hotwords_cache && ENABLE_HOTWORDS_CACHE &&
vm->hotwords_cache->enabled)
{
spec_entry->execution_heat = HOTWORDS_EXECUTION_HEAT_THRESHOLD + 1;
hotwords_cache_promote(vm->hotwords_cache, spec_entry);
prev->transition_metrics->prefetch_attempts++;
vm->pipeline_metrics.prefetch_attempts++;
}
}
}
/**
* @brief Post-execution physics hook — called after every word dispatch.
*
* Consolidates all physics work that must happen after a word completes:
* - Updates physics metadata (@c temperature_q8, @c last_active_ns) via
* @c physics_metadata_touch()
* - Notifies the profiler (@c profiler_word_exit())
* - Increments @c vm->heartbeat.words_executed
* - When running without a background heartbeat thread, checks whether
* enough words have been executed to trigger a heartbeat cycle
* (@c vm_heartbeat_run_cycle())
*
* @param vm Pointer to the VM structure
* @param word DictEntry that just finished executing
*/
void physics_post_execute(VM* vm, DictEntry* word)
{
if (!vm || !word) return;
physics_metadata_touch(word, word->execution_heat, sf_monotonic_ns());
profiler_word_exit(word);
vm->heartbeat.words_executed++;
/* Heartbeat: Periodic time-driven tuning (Loop #3 & #5) */
if (!vm->heartbeat.worker && ++vm->heartbeat.check_counter >= HEARTBEAT_CHECK_FREQUENCY)
{
extern void vm_heartbeat_run_cycle(VM* vm);
vm_heartbeat_run_cycle(vm);
vm->heartbeat.check_counter = 0;
}
}
/**
* @brief Physics hook for the outer interpreter word-lookup path.
*
* Called when the outer interpreter successfully finds a word by name,
* before executing it. Applies linear decay and increments heat on both
* the found entry and the canonical entry (if different), and updates
* @c last_active_ns / @c last_decay_ns on both. Acquires @c vm->dict_lock
* for the entire operation to ensure thread safety.
*
* @param vm Pointer to the VM structure
* @param entry DictEntry found by name search
* @param canon Canonical DictEntry (may equal @c entry or be the smudge target)
*/
void physics_on_lookup(VM* vm, DictEntry* entry, DictEntry* canon)
{
if (!vm || !entry) return;
uint64_t lookup_ns = sf_monotonic_ns();
sf_mutex_lock(&vm->dict_lock);
/* Apply decay and heat to found entry. Tick-based, not wall-clock --
* see physics_metadata_apply_linear_decay(). */
uint64_t elapsed_entry_ticks = vm->heartbeat.tick_count - entry->physics.last_decay_tick;
physics_metadata_apply_linear_decay(entry, elapsed_entry_ticks, vm);
entry->physics.last_decay_tick = vm->heartbeat.tick_count;
entry->physics.last_active_ns = lookup_ns;
entry->physics.last_decay_ns = lookup_ns;
physics_execution_heat_increment(entry);
/* Also handle canonical entry if different */
if (canon && canon != entry)
{
uint64_t elapsed_canon_ticks = vm->heartbeat.tick_count - canon->physics.last_decay_tick;
physics_metadata_apply_linear_decay(canon, elapsed_canon_ticks, vm);
canon->physics.last_decay_tick = vm->heartbeat.tick_count;
canon->physics.last_active_ns = lookup_ns;
canon->physics.last_decay_ns = lookup_ns;
physics_execution_heat_increment(canon);
physics_metadata_touch(canon, canon->execution_heat, lookup_ns);
}
sf_mutex_unlock(&vm->dict_lock);
}
+991
View File
@@ -0,0 +1,991 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* physics_hotwords_cache.c
*
* Hot-words cache implementation
* Provides frequency-driven dictionary acceleration via LRU cache
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "physics_hotwords_cache.h"
/* In kernel builds, suppress platform_time.h inline wrappers to avoid
* GOTPCREL relocations in PE binaries (no GOT in PE format). shim.c
* provides the concrete sf_monotonic_ns() symbol in that case.
* PLATFORM_TIME_NO_INLINE is also set globally via COMMON_CFLAGS in
* Makefile.starkernel; the #ifndef guard prevents -Wall -Werror redefinition. */
#ifdef __STARKERNEL__
#ifndef PLATFORM_TIME_NO_INLINE
#define PLATFORM_TIME_NO_INLINE
#endif
#endif /* __STARKERNEL__ */
#include "platform_time.h"
#include "math_portable.h"
/* ============================================================================
* Global Cache Instance
* ============================================================================
*/
/* Global hot-words cache for the VM (used by benchmark and diagnostic words) */
HotwordsCache g_hotwords_cache = {0};
/* Forward declarations for helper functions */
static inline double q48_to_ns_double(int64_t q48);
/* ============================================================================
* Cache Initialization
* ============================================================================
*/
/**
* @brief Initialise a @c HotwordsCache to its empty, enabled state.
*
* Zeroes all fields via @c memset then explicitly sets:
* - @c cache->enabled = @c ENABLE_HOTWORDS_CACHE (build-time compile flag;
* 0 disables the cache globally without a code change).
* - @c cache->cache_count = 0, @c cache->lru_index = 0 — empty round-robin
* eviction ring.
* - All counter fields in @c cache->stats to 0.
* - @c stats.min_cache_hit_ns = @c INT64_MAX, @c stats.max_cache_hit_ns =
* @c INT64_MIN (sentinel values; updated to real observations on the first
* cache hit).
* - @c stats.min_bucket_search_ns = @c INT64_MAX, @c stats.max_bucket_search_ns
* = @c INT64_MIN (same sentinel convention for bucket searches).
* - All Q48.16 variance accumulators to 0.
*
* Safe to call multiple times (idempotent via @c memset). Must be called
* before the first @c hotwords_cache_lookup() invocation; failure to do so
* leaves the sentinel min/max fields uninitialised, producing nonsense in the
* Bayesian posterior report.
*
* @param cache Cache to initialise; no-op if NULL.
*/
void hotwords_cache_init(HotwordsCache *cache) {
if (!cache) return;
memset(cache, 0, sizeof(*cache));
cache->enabled = ENABLE_HOTWORDS_CACHE;
cache->cache_count = 0;
cache->lru_index = 0;
/* Initialize statistics with proper sentinel values */
cache->stats.total_lookups = 0;
cache->stats.cache_hits = 0;
cache->stats.bucket_hits = 0;
cache->stats.misses = 0;
cache->stats.evictions = 0;
cache->stats.promotions = 0;
cache->stats.bucket_reorders = 0;
/* Fixed-point statistics (Q48.16 format) */
cache->stats.cache_hit_total_ns_q48 = 0;
cache->stats.bucket_search_total_ns_q48 = 0;
cache->stats.cache_hit_samples = 0;
cache->stats.bucket_search_samples = 0;
/* Min/max latencies */
cache->stats.min_cache_hit_ns = INT64_MAX;
cache->stats.max_cache_hit_ns = INT64_MIN;
cache->stats.min_bucket_search_ns = INT64_MAX;
cache->stats.max_bucket_search_ns = INT64_MIN;
/* Variance accumulators (Q48.16) */
cache->stats.cache_hit_variance_sum_q48 = 0;
cache->stats.bucket_search_variance_sum_q48 = 0;
}
/**
* @brief Zero all fields in a @c HotwordsCache, releasing no heap memory.
*
* Calls @c memset(cache, 0, sizeof(*cache)) to scrub every field including
* the inline @c cache[] pointer array and all statistics. The cache holds no
* separately allocated memory (the @c DictEntry pointers inside @c cache[]
* are owned by the dictionary, not the cache), so no @c free() calls are
* needed.
*
* Intended for use at VM teardown or when the caller wants to reset the cache
* to a known-zero state between benchmark runs. After this call the cache
* is in an unusable state (@c enabled = 0); call @c hotwords_cache_init()
* to reinitialise it.
*
* @param cache Cache to zero; no-op if NULL.
*/
void hotwords_cache_cleanup(HotwordsCache *cache) {
if (!cache) return;
memset(cache, 0, sizeof(*cache));
}
/* ============================================================================
* Main Lookup: Cache + Bucket Search
* ============================================================================
*/
/**
* @brief Look up a FORTH word using the hot-words cache, falling back to the bucket.
*
* The primary dictionary fast-path. Records a start timestamp via
* @c sf_monotonic_ns() and then executes a two-stage search:
*
* **Stage 1 — Cache check (O(HOTWORDS_CACHE_SIZE)):**
* Scans the @c cache->cache[] ring for an entry whose @c name_len and name
* match @p name/@p len, using the cheap-first filter (@c name_len →
* last-character → @c memcmp). On a hit the elapsed nanoseconds are
* converted to Q48.16 and accumulated into @c stats.cache_hit_total_ns_q48;
* the squared value is accumulated into @c stats.cache_hit_variance_sum_q48
* for Bayesian posterior computation by @c calculate_posterior(). Min/max
* latency sentinels are also updated.
*
* **Stage 2 — Bucket fallback (O(bucket_count)):**
* Scans @p bucket[] in reverse-insertion order (newest definition wins).
* On a hit the elapsed time is recorded in the @c bucket_search_* accumulators
* with the same Q48.16 and variance treatment as Stage 1. If the found
* word's @c execution_heat exceeds @c HOTWORDS_EXECUTION_HEAT_THRESHOLD it
* is promoted into the cache via @c hotwords_cache_promote().
*
* **Disabled path:** If @c cache is NULL or @c cache->enabled is 0, the
* function skips Stage 1 and performs a bare reverse-linear scan of @p bucket[]
* returning the result immediately (no statistics are recorded).
*
* A miss increments @c stats.misses and returns NULL.
*
* @param cache Hot-words cache; may be NULL or disabled (falls back
* to bucket scan without statistics).
* @param bucket First-character hash bucket — array of @c DictEntry*
* pointers, searched in reverse order.
* @param bucket_count Number of valid pointers in @p bucket.
* @param name Word name to find (not NUL-terminated).
* @param len Length of @p name in bytes.
* @return Matching @c DictEntry*, or NULL if not found.
*/
DictEntry *hotwords_cache_lookup(VM *vm,
HotwordsCache *cache,
DictEntry **bucket,
size_t bucket_count,
const char *name,
size_t len) {
uint64_t start_ns = sf_monotonic_ns();
// Disabled? Go straight to the arbitrated bucket resolver
if (!cache || !cache->enabled) {
return vm_dict_resolve_in_bucket(vm, bucket, bucket_count, name, len);
}
cache->stats.total_lookups++;
// --- STEP 1: Check hot-words cache ---
for (uint32_t i = 0; i < cache->cache_count; i++) {
DictEntry *e = cache->cache[i];
if (!e) continue;
if ((size_t)e->name_len != len) continue;
#ifdef WORD_HIDDEN
if (e->flags & WORD_HIDDEN) continue;
#endif
#ifdef WORD_SMUDGED
if (e->flags & WORD_SMUDGED) continue;
#endif
if (len > 1 && (unsigned char)e->name[len - 1] != (unsigned char)name[len - 1]) continue;
if (memcmp(e->name, name, len) == 0) {
// Cache hit! Record latency with 64-bit fixed-point precision
cache->stats.cache_hits++;
int64_t elapsed_ns = (int64_t)(sf_monotonic_ns() - start_ns);
/* Store in Q48.16 fixed-point format (shift left by 16 bits for fractional precision) */
cache->stats.cache_hit_total_ns_q48 += (elapsed_ns << 16);
cache->stats.cache_hit_samples++;
/* Track min/max */
if (elapsed_ns < cache->stats.min_cache_hit_ns) {
cache->stats.min_cache_hit_ns = elapsed_ns;
}
if (elapsed_ns > cache->stats.max_cache_hit_ns) {
cache->stats.max_cache_hit_ns = elapsed_ns;
}
/* Accumulate squared values for variance calculation (Bayesian posteriors) */
int64_t elapsed_sq_q48 = (elapsed_ns * elapsed_ns) << 16;
cache->stats.cache_hit_variance_sum_q48 += elapsed_sq_q48;
return e;
}
}
// --- STEP 2: Fall back to bucket search (arbitrated: newest visible
// same-named definition wins regardless of heat ordering) ---
{
DictEntry *e = vm_dict_resolve_in_bucket(vm, bucket, bucket_count, name, len);
if (e) {
// Bucket hit! Record latency with 64-bit fixed-point precision
cache->stats.bucket_hits++;
int64_t elapsed_ns = (int64_t)(sf_monotonic_ns() - start_ns);
/* Store in Q48.16 fixed-point format (shift left by 16 bits for fractional precision) */
cache->stats.bucket_search_total_ns_q48 += (elapsed_ns << 16);
cache->stats.bucket_search_samples++;
/* Track min/max */
if (elapsed_ns < cache->stats.min_bucket_search_ns) {
cache->stats.min_bucket_search_ns = elapsed_ns;
}
if (elapsed_ns > cache->stats.max_bucket_search_ns) {
cache->stats.max_bucket_search_ns = elapsed_ns;
}
/* Accumulate squared values for variance calculation (Bayesian posteriors) */
int64_t elapsed_sq_q48 = (elapsed_ns * elapsed_ns) << 16;
cache->stats.bucket_search_variance_sum_q48 += elapsed_sq_q48;
// If hot enough, promote to cache
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD) {
hotwords_cache_promote(cache, e);
}
return e;
}
}
// Miss
cache->stats.misses++;
return NULL;
}
/* ============================================================================
* Cache Eviction (coherence with dictionary mutations)
* ============================================================================
*/
/* Remove ring slot i, compacting the tail down one position. */
static void hotwords_cache_remove_at(HotwordsCache *cache, uint32_t i) {
for (uint32_t j = i; j + 1 < cache->cache_count; j++) {
cache->cache[j] = cache->cache[j + 1];
}
cache->cache_count--;
cache->cache[cache->cache_count] = NULL;
if (cache->lru_index >= cache->cache_count) {
cache->lru_index = 0;
}
}
void hotwords_cache_evict_entry(HotwordsCache *cache, const DictEntry *word) {
if (!cache || !word || cache->cache_count == 0) return;
for (uint32_t i = cache->cache_count; i-- > 0;) {
if (cache->cache[i] == word) {
hotwords_cache_remove_at(cache, i);
}
}
}
void hotwords_cache_evict_name(HotwordsCache *cache, const char *name, size_t len) {
if (!cache || !name || len == 0 || cache->cache_count == 0) return;
for (uint32_t i = cache->cache_count; i-- > 0;) {
DictEntry *e = cache->cache[i];
if (!e) continue;
if ((size_t)e->name_len != len) continue;
if (memcmp(e->name, name, len) != 0) continue;
hotwords_cache_remove_at(cache, i);
}
}
/* ============================================================================
* Cache Promotion (LRU eviction)
* ============================================================================
*/
/**
* @brief Promote a @c DictEntry into the hot-words cache, evicting the oldest entry if full.
*
* Two paths depending on whether the cache has a free slot:
*
* - **Slot available** (@c cache->cache_count < @c HOTWORDS_CACHE_SIZE):
* Appends @p word at @c cache->cache[cache_count++] after verifying it is
* not already present (linear scan). Increments @c stats.promotions.
*
* - **Cache full** (@c cache->cache_count >= @c HOTWORDS_CACHE_SIZE):
* Overwrites @c cache->cache[cache->lru_index] with @p word (round-robin
* least-recently-used eviction) and advances @c lru_index modulo
* @c HOTWORDS_CACHE_SIZE. Increments @c stats.evictions. The evicted entry
* is not freed — it remains alive in the dictionary; only its slot in the
* cache ring is surrendered.
*
* Called from @c hotwords_cache_lookup() whenever a bucket search finds a
* word hot enough to warrant caching (heat > @c HOTWORDS_EXECUTION_HEAT_THRESHOLD).
*
* @param cache The cache to promote into; no-op if NULL.
* @param word The @c DictEntry to promote; no-op if NULL. Ownership is not
* transferred — the cache holds a borrowed pointer into the
* dictionary, which owns the entry.
*/
void hotwords_cache_promote(HotwordsCache *cache, DictEntry *word) {
if (!cache || !word || cache->cache_count >= HOTWORDS_CACHE_SIZE) {
if (cache && cache->cache_count >= HOTWORDS_CACHE_SIZE) {
// LRU eviction: remove oldest entry (round-robin)
cache->cache[cache->lru_index] = word;
cache->lru_index = (cache->lru_index + 1) % HOTWORDS_CACHE_SIZE;
cache->stats.evictions++;
}
return;
}
// Check if already in cache
for (uint32_t i = 0; i < cache->cache_count; i++) {
if (cache->cache[i] == word) {
return; // Already cached
}
}
// Add to cache
cache->cache[cache->cache_count++] = word;
cache->stats.promotions++;
}
/* ============================================================================
* Bucket Reordering (move hot words forward)
* ============================================================================
*/
/**
* @brief Sort a hash bucket in-place by descending execution heat (bubble sort).
*
* Performs a classic in-place bubble sort over the @p bucket array so that
* words with higher @c execution_heat appear at lower array indices. The
* hot-words cache and heat-stratified lookup both benefit from having the
* hottest words at the front of each bucket: the first-character hash buckets
* are linear arrays, and placing hot words up front reduces the scan depth on
* Stage 1 cache misses that fall through to Stage 2 bucket search.
*
* Bubble sort is chosen deliberately: buckets are small (typically single
* digits to low dozens of words per first-character class), already nearly
* sorted after the previous reorganization pass, and the cost of a O(n²)
* inner loop on n ≤ 30 is negligible compared to the outer @c qsort on
* all 256 buckets in @c dict_reorganize_buckets_by_heat(). For the individual
* per-bucket call path (e.g. after a single promotion), bubble sort avoids
* the @c malloc overhead that @c qsort's temporary storage would require.
*
* No-op for @p count < 2.
*
* @param bucket Array of @c DictEntry* pointers to sort in-place.
* NULL-check is the caller's responsibility.
* @param count Number of valid entries in @p bucket.
*/
void hotwords_bucket_reorder(DictEntry **bucket, size_t count) {
if (!bucket || count < 2) return;
// Simple bubble sort: move high-execution-heat words forward
for (size_t i = 0; i < count - 1; i++) {
for (size_t j = 0; j < count - i - 1; j++) {
if (bucket[j]->execution_heat < bucket[j + 1]->execution_heat) {
// Swap
DictEntry *tmp = bucket[j];
bucket[j] = bucket[j + 1];
bucket[j + 1] = tmp;
}
}
}
}
/* ============================================================================
* Statistics
* ============================================================================
*/
/**
* @brief Zero all fields in a @c HotwordsStats structure.
*
* Resets every counter (@c total_lookups, @c cache_hits, @c bucket_hits,
* @c misses, @c evictions, @c promotions, @c bucket_reorders) to zero,
* resets all Q48.16 accumulator fields (@c cache_hit_total_ns_q48,
* @c bucket_search_total_ns_q48, @c cache_hit_variance_sum_q48,
* @c bucket_search_variance_sum_q48) to zero, resets sample counts to zero,
* and restores the min/max latency sentinels:
* - @c min_*_ns = @c INT64_MAX
* - @c max_*_ns = @c INT64_MIN
*
* Intended for DoE benchmark runs that need a clean statistical baseline
* between treatment cells. The parent @c HotwordsCache also holds the
* @c cache[] pointer array and LRU state; those are not touched — only the
* @c HotwordsStats sub-struct is cleared.
*
* @param stats Statistics structure to reset; no-op if NULL.
*/
void hotwords_stats_reset(HotwordsStats *stats) {
if (!stats) return;
/* Reset counters */
stats->total_lookups = 0;
stats->cache_hits = 0;
stats->bucket_hits = 0;
stats->misses = 0;
stats->evictions = 0;
stats->promotions = 0;
stats->bucket_reorders = 0;
/* Reset fixed-point accumulators */
stats->cache_hit_total_ns_q48 = 0;
stats->bucket_search_total_ns_q48 = 0;
stats->cache_hit_samples = 0;
stats->bucket_search_samples = 0;
/* Reset min/max latencies */
stats->min_cache_hit_ns = INT64_MAX;
stats->max_cache_hit_ns = INT64_MIN;
stats->min_bucket_search_ns = INT64_MAX;
stats->max_bucket_search_ns = INT64_MIN;
/* Reset variance accumulators */
stats->cache_hit_variance_sum_q48 = 0;
stats->bucket_search_variance_sum_q48 = 0;
}
/**
* @brief Print a formatted hot-words cache statistics report to @c stdout.
*
* Emits a Unicode box-drawing table covering three sections:
*
* **LOOKUPS** — total, cache hits, bucket hits, misses with percentages.
*
* **LATENCY STATISTICS** — for each of cache-hits and bucket-searches
* (when their sample counts are non-zero):
* - Min / avg / max in nanoseconds, derived from the Q48.16 accumulators
* by right-shifting 16 bits: @c avg_ns = @c total_ns_q48 / samples >> 16.
* - Standard deviation computed from the Welford-style variance accumulator:
* @c var = E[X²] − E[X]², then @c stddev = sqrt_q48(var), converted to
* nanoseconds via @c q48_to_ns_double() for display only.
* - Speedup ratio (bucket avg / cache avg) when both sample sets are present.
*
* **CACHE MANAGEMENT** — promotions, evictions, and bucket reorder counts.
*
* If @p stats is NULL or the total lookup count is zero, prints a brief
* "No lookups performed." notice instead of the full table. All floating-point
* usage in this function is display-only (via @c q48_to_ns_double()); the
* underlying accumulators remain in Q48.16 integer format.
*
* @param stats Statistics to report; no-op if NULL.
*/
void hotwords_stats_print(const HotwordsStats *stats) {
if (!stats) return;
printf("\n╔════════════════════════════════════════════════════════════════╗\n");
printf("║ Hot-Words Cache Statistics (64-bit Fixed-Point Q48.16) ║\n");
printf("╚════════════════════════════════════════════════════════════════╝\n\n");
uint64_t total = stats->cache_hits + stats->bucket_hits + stats->misses;
if (total == 0) {
printf("No lookups performed.\n");
return;
}
double cache_hit_pct = 100.0 * stats->cache_hits / total;
double bucket_hit_pct = 100.0 * stats->bucket_hits / total;
double miss_pct = 100.0 * stats->misses / total;
printf("LOOKUPS (Statistically Valid Sample Size):\n");
printf(" Total: %lu\n", total);
printf(" Cache hits: %lu (%.2f%%)\n", stats->cache_hits, cache_hit_pct);
printf(" Bucket hits: %lu (%.2f%%)\n", stats->bucket_hits, bucket_hit_pct);
printf(" Misses: %lu (%.2f%%)\n", stats->misses, miss_pct);
printf("\nLATENCY STATISTICS (64-bit Fixed-Point Precision):\n");
/* Cache hit latency (Q48.16 format: shift right by 16 bits to get nanoseconds) */
if (stats->cache_hit_samples > 0) {
int64_t avg_cache_ns_q48 = stats->cache_hit_total_ns_q48 / (int64_t)stats->cache_hit_samples;
double avg_cache_ns = (double)avg_cache_ns_q48 / 65536.0; /* Q48.16 to double */
double min_cache_ns = (double)stats->min_cache_hit_ns;
double max_cache_ns = (double)stats->max_cache_hit_ns;
printf(" Cache Hits (%lu samples):\n", stats->cache_hit_samples);
printf(" Min: %.3f ns\n", min_cache_ns);
printf(" Avg: %.3f ns\n", avg_cache_ns);
printf(" Max: %.3f ns\n", max_cache_ns);
/* Calculate variance and standard deviation */
int64_t variance_sum_q48 = stats->cache_hit_variance_sum_q48 / (int64_t)stats->cache_hit_samples;
int64_t mean_sq_q48 = (avg_cache_ns_q48 * avg_cache_ns_q48) / (1LL << 16);
int64_t var_q48 = variance_sum_q48 - mean_sq_q48;
int64_t stddev_q48 = (var_q48 >= 0) ? sqrt_q48(var_q48) : 0;
double stddev = q48_to_ns_double(stddev_q48);
printf(" StdDev: %.3f ns\n", stddev);
}
if (stats->bucket_search_samples > 0) {
int64_t avg_bucket_ns_q48 = stats->bucket_search_total_ns_q48 / (int64_t)stats->bucket_search_samples;
double avg_bucket_ns = (double)avg_bucket_ns_q48 / 65536.0; /* Q48.16 to double */
double min_bucket_ns = (double)stats->min_bucket_search_ns;
double max_bucket_ns = (double)stats->max_bucket_search_ns;
printf(" Bucket Searches (%lu samples):\n", stats->bucket_search_samples);
printf(" Min: %.3f ns\n", min_bucket_ns);
printf(" Avg: %.3f ns\n", avg_bucket_ns);
printf(" Max: %.3f ns\n", max_bucket_ns);
/* Calculate variance and standard deviation */
int64_t variance_sum_q48 = stats->bucket_search_variance_sum_q48 / (int64_t)stats->bucket_search_samples;
int64_t mean_sq_q48 = (avg_bucket_ns_q48 * avg_bucket_ns_q48) / (1LL << 16);
int64_t var_q48 = variance_sum_q48 - mean_sq_q48;
int64_t stddev_q48 = (var_q48 >= 0) ? sqrt_q48(var_q48) : 0;
double stddev = q48_to_ns_double(stddev_q48);
printf(" StdDev: %.3f ns\n", stddev);
/* Speedup calculation */
if (stats->cache_hit_samples > 0) {
int64_t avg_cache_ns_q48 = stats->cache_hit_total_ns_q48 / (int64_t)stats->cache_hit_samples;
double avg_bucket_ns_dbl = (double)avg_bucket_ns_q48 / 65536.0;
double speedup = avg_bucket_ns_dbl / ((double)avg_cache_ns_q48 / 65536.0);
printf(" Speedup: %.2f× (bucket vs cache)\n", speedup);
}
}
printf("\nCACHE MANAGEMENT:\n");
printf(" Promotions: %lu\n", stats->promotions);
printf(" Evictions: %lu\n", stats->evictions);
printf(" Reorders: %lu\n", stats->bucket_reorders);
printf("\n════════════════════════════════════════════════════════════════\n\n");
}
/**
* @brief Enable or disable the hot-words cache at runtime.
*
* Sets @c cache->enabled to @p enabled (non-zero = active, 0 = bypassed).
* When disabled, @c hotwords_cache_lookup() skips Stage 1 entirely and
* falls through directly to a bare bucket scan, recording no statistics.
* This allows benchmark harnesses to compare cache-on vs. cache-off behaviour
* without rebuilding the binary.
*
* @param cache Cache to configure; no-op if NULL.
* @param enabled Non-zero to enable the cache; 0 to disable.
*/
void hotwords_cache_set_enabled(HotwordsCache *cache, int enabled) {
if (!cache) return;
cache->enabled = enabled;
}
/* ============================================================================
* Bayesian Inference Implementation
* ============================================================================
*/
/**
* @brief Convert a Q48.16 fixed-point value to a @c double nanosecond count.
*
* Divides @p q48 by 65536.0 to recover the real-valued nanosecond count.
* This is the only floating-point operation in the module and is intentionally
* confined to display paths — @c hotwords_stats_print() and
* @c hotwords_bayesian_report() — so that all internal accumulation and
* statistical computation remains in Q48.16 integer arithmetic.
*
* @param q48 Value in Q48.16 format (1.0 ns = 65536).
* @return The same quantity as a @c double in nanoseconds.
*/
static inline double q48_to_ns_double(int64_t q48) {
return (double)q48 / 65536.0;
}
/**
* @brief Compute a Bayesian latency posterior for one measurement distribution.
*
* Given the accumulated Q48.16 statistics for a set of @p sample_count
* timing observations (either cache-hit or bucket-search latencies), computes
* the following posterior quantities entirely in Q48.16 integer arithmetic:
*
* - **Mean** (@c mean_ns_q48): @c total_ns_q48 / sample_count.
* - **Variance** (@c stddev_ns_q48): @c E[X²] − @c E[X]² where @c E[X²] is
* the per-sample average of @p variance_sum_q48 and @c E[X]² =
* @c mean² scaled to Q48.16 via @c >> 16. Variance is clamped to 0 to
* guard numerical rounding.
* - **Standard deviation** (@c stddev_ns_q48): @c sqrt_q48(variance).
* - **Median** (@c median_ns_q48): @c (min_ns + max_ns) / 2 — a quick
* midpoint estimate rather than a true median, consistent across all
* call sites.
* - **Standard error** (@c se_q48): @c stddev / √N, computed by converting
* @p sample_count to Q48.16 (@c sample_count << 16) and calling
* @c sqrt_q48(), then dividing @c stddev << 16 by the result.
* - **95% credible interval**: @c mean ± Z₉₅ × SE where
* @c Z₉₅ = 128431 (1.96 × 65536).
* - **99% credible interval**: @c mean ± Z₉₉ × SE where
* @c Z₉₉ = 168888 (2.576 × 65536).
* Lower credible bounds are clamped at 0 (latency is non-negative).
*
* Returns a zeroed @c BayesianLatencyPosterior if @p sample_count is 0.
*
* @param total_ns_q48 Sum of all latency observations in Q48.16.
* @param variance_sum_q48 Sum of squared latency observations in Q48.16
* (i.e., @c Σ(xᵢ² << 16)).
* @param min_ns Minimum observed latency in integer nanoseconds.
* @param max_ns Maximum observed latency in integer nanoseconds.
* @param sample_count Number of observations; 0 → returns zeroed struct.
* @return @c BayesianLatencyPosterior with all fields in Q48.16.
*/
static BayesianLatencyPosterior calculate_posterior(
int64_t total_ns_q48,
int64_t variance_sum_q48,
int64_t min_ns,
int64_t max_ns,
uint64_t sample_count) {
const int64_t Z95 = 128431LL; /* 1.96 * 65536 */
const int64_t Z99 = 168888LL; /* 2.576 * 65536 */
BayesianLatencyPosterior posterior = {0};
posterior.sample_count = sample_count;
if (sample_count == 0) {
return posterior;
}
/* Calculate mean from accumulated Q48.16 values (pure integer division) */
int64_t mean_q48 = total_ns_q48 / (int64_t)sample_count;
posterior.mean_ns_q48 = mean_q48;
/* Calculate variance: E[X^2] - E[X]^2, all in Q48.16 */
int64_t mean_sq_q48 = (mean_q48 * mean_q48) >> 16;
int64_t variance_sum_corrected = variance_sum_q48 / (int64_t)sample_count;
int64_t variance_q48 = variance_sum_corrected - mean_sq_q48;
if (variance_q48 < 0) variance_q48 = 0; /* Guard numerical errors */
/* Calculate standard deviation using Q48.16 sqrt */
int64_t stddev_q48 = sqrt_q48(variance_q48);
posterior.stddev_ns_q48 = stddev_q48;
/* Median: average of min/max */
posterior.median_ns_q48 = (min_ns + max_ns) / 2;
/* Standard error: stddev / sqrt(N) in Q48.16 */
/* sqrt(sample_count) approximation for Q48.16 arithmetic */
int64_t sqrt_n_q48 = sqrt_q48((int64_t)sample_count << 16); /* Convert N to Q48.16 */
int64_t se_q48 = (stddev_q48 << 16) / (sqrt_n_q48 >> 16); /* Careful division */
/* 95% credible interval: mean ± Z95 * SE (Z95 = 1.96 << 16) */
int64_t margin_95 = (Z95 * se_q48) >> 16;
posterior.credible_lower_95 = mean_q48 - margin_95;
posterior.credible_upper_95 = mean_q48 + margin_95;
/* 99% credible interval: mean ± Z99 * SE (Z99 = 2.576 << 16) */
int64_t margin_99 = (Z99 * se_q48) >> 16;
posterior.credible_lower_99 = mean_q48 - margin_99;
posterior.credible_upper_99 = mean_q48 + margin_99;
/* Clamp lower bounds to >= 0 */
if (posterior.credible_lower_95 < 0) posterior.credible_lower_95 = 0;
if (posterior.credible_lower_99 < 0) posterior.credible_lower_99 = 0;
return posterior;
}
/**
* @brief Return the Bayesian latency posterior for cache-hit observations.
*
* Forwards the cache-hit accumulators from @p stats to @c calculate_posterior():
* @c cache_hit_total_ns_q48, @c cache_hit_variance_sum_q48, @c min_cache_hit_ns,
* @c max_cache_hit_ns, and @c cache_hit_samples. Returns a zeroed
* @c BayesianLatencyPosterior if @p stats is NULL or no cache-hit samples have
* been recorded yet.
*
* The returned posterior's @c mean_ns_q48, @c stddev_ns_q48, and 95%/99%
* credible-interval fields are consumed by @c hotwords_speedup_estimate() and
* @c hotwords_bayesian_report() to compare cache-hit latency against bucket-search
* latency.
*
* @param stats @c HotwordsStats containing cache-hit timing accumulators.
* @return Posterior in Q48.16; all fields zero if no samples exist.
*/
BayesianLatencyPosterior hotwords_posterior_cache_hits(const HotwordsStats *stats) {
if (!stats || stats->cache_hit_samples == 0) {
BayesianLatencyPosterior empty = {0};
return empty;
}
return calculate_posterior(
stats->cache_hit_total_ns_q48,
stats->cache_hit_variance_sum_q48,
stats->min_cache_hit_ns,
stats->max_cache_hit_ns,
stats->cache_hit_samples
);
}
/**
* @brief Return the Bayesian latency posterior for bucket-search observations.
*
* Forwards the bucket-search accumulators from @p stats to @c calculate_posterior():
* @c bucket_search_total_ns_q48, @c bucket_search_variance_sum_q48,
* @c min_bucket_search_ns, @c max_bucket_search_ns, and @c bucket_search_samples.
* Returns a zeroed @c BayesianLatencyPosterior if @p stats is NULL or no
* bucket-search samples have been recorded.
*
* The returned posterior's mean and credible-interval fields feed
* @c hotwords_speedup_estimate() (as the slower distribution) and the
* baseline section of @c hotwords_bayesian_report().
*
* @param stats @c HotwordsStats containing bucket-search timing accumulators.
* @return Posterior in Q48.16; all fields zero if no samples exist.
*/
BayesianLatencyPosterior hotwords_posterior_bucket_searches(const HotwordsStats *stats) {
if (!stats || stats->bucket_search_samples == 0) {
BayesianLatencyPosterior empty = {0};
return empty;
}
return calculate_posterior(
stats->bucket_search_total_ns_q48,
stats->bucket_search_variance_sum_q48,
stats->min_bucket_search_ns,
stats->max_bucket_search_ns,
stats->bucket_search_samples
);
}
/**
* @brief Compute a speedup estimate with credible intervals using the delta method.
*
* Derives a dimensionless speedup ratio @c S = bucket_mean / cache_mean in
* Q48.16 fixed-point and propagates uncertainty via the delta method for ratios:
* @code
* SE(S) ≈ S × sqrt( (SE_bucket / bucket_mean)² + (SE_cache / cache_mean)² )
* @endcode
* All arithmetic is Q48.16 integer throughout; the only deviation is the
* intermediate @c sqrt() call which also runs in Q48.16 via @c sqrt_q48().
*
* Computed fields in the returned @c SpeedupEstimate (all Q48.16):
* - @c speedup_factor_q48 — point estimate of @c S.
* - @c credible_lower_95_q48 / @c credible_upper_95_q48 — 95% credible
* interval: @c S ± 1.96 × SE(S) (Z₉₅ = 128431 in Q48.16). Lower bound
* clamped at 0.
* - @c probability_gt_10pct_q48 — P(speedup > 1.1×), computed as
* @c 0.5 × (1 + erf(z / √2)) where @c z = (1.1 − S) / SE(S) is negated
* to give the right-tail probability and @c √2 = 92681 in Q48.16.
* - @c probability_gt_double_q48 — P(speedup > 2.0×), same formula with
* target = 2.0 × 65536 = 131072.
*
* Returns a default @c SpeedupEstimate with @c speedup_factor_q48 = 65536
* (1.0×) if either distribution has zero samples or if @p stats is NULL.
*
* @param stats @c HotwordsStats with both cache-hit and bucket-search samples.
* @return @c SpeedupEstimate with all fields in Q48.16 fixed-point.
*/
struct SpeedupEstimate hotwords_speedup_estimate(const HotwordsStats *stats) {
const int64_t Q48 = 65536LL;
const int64_t SQRT2 = 92681LL; /* sqrt(2) * 65536 ≈ 1.414 * 65536 */
struct SpeedupEstimate est = {0};
if (!stats || stats->cache_hit_samples == 0 || stats->bucket_search_samples == 0) {
est.speedup_factor_q48 = Q48; /* Default: 1.0x speedup */
return est;
}
BayesianLatencyPosterior cache_post = hotwords_posterior_cache_hits(stats);
BayesianLatencyPosterior bucket_post = hotwords_posterior_bucket_searches(stats);
/* Guard divide-by-zero */
if (cache_post.mean_ns_q48 < 1) cache_post.mean_ns_q48 = 1;
/* Point estimate: speedup = bucket_time / cache_time (dimensionless, in Q48.16) */
int64_t speedup_q48 = (bucket_post.mean_ns_q48 << 16) / cache_post.mean_ns_q48;
est.speedup_factor_q48 = speedup_q48;
/* Delta-method SE for ratio: SE(speedup) ≈ speedup * sqrt((SE_bucket/bucket)^2 + (SE_cache/cache)^2) */
int64_t se_cache_q48 = (cache_post.stddev_ns_q48 << 16) / (int64_t)stats->cache_hit_samples;
int64_t se_bucket_q48 = (bucket_post.stddev_ns_q48 << 16) / (int64_t)stats->bucket_search_samples;
/* Relative standard errors: SE/mean */
int64_t rel_se_cache_q48 = (se_cache_q48 << 16) / cache_post.mean_ns_q48;
int64_t rel_se_bucket_q48 = (se_bucket_q48 << 16) / bucket_post.mean_ns_q48;
/* Combine: sqrt((rel_se_cache)^2 + (rel_se_bucket)^2) */
int64_t sum_sq = ((rel_se_cache_q48 * rel_se_cache_q48) >> 16) +
((rel_se_bucket_q48 * rel_se_bucket_q48) >> 16);
int64_t rel_se_ratio_q48 = sqrt_q48(sum_sq);
/* SE(speedup) = speedup * rel_se_ratio */
int64_t se_speedup_q48 = (speedup_q48 * rel_se_ratio_q48) >> 16;
/* 95% credible interval for speedup (Z1.96 = 128431 in Q48.16) */
int64_t margin_95_q48 = (128431LL * se_speedup_q48) >> 16;
est.credible_lower_95_q48 = speedup_q48 - margin_95_q48;
est.credible_upper_95_q48 = speedup_q48 + margin_95_q48;
if (est.credible_lower_95_q48 < 0) est.credible_lower_95_q48 = 0;
/* Probability of speedup > 1.1× using erf_q48 (pure fixed-point) */
/* Z-score: (1.1 - speedup) / SE_speedup, computed in Q48.16 */
/* Result: erf returns Q48.16 in [-65536, 65536], convert to probability [0, 65536] */
int64_t target_110_q48 = 72090LL; /* 1.1 * 65536 */
int64_t z_10pct_q48 = ((target_110_q48 - speedup_q48) << 16) / se_speedup_q48;
int64_t erf_val_10pct = erf_q48((-z_10pct_q48) / SQRT2);
/* Convert erf result to probability: P = 0.5 * (1 + erf) in Q48.16 */
est.probability_gt_10pct_q48 = (Q48 + erf_val_10pct) >> 1;
/* Probability of speedup > 2.0× */
int64_t target_200_q48 = 131072LL; /* 2.0 * 65536 */
int64_t z_2x_q48 = ((target_200_q48 - speedup_q48) << 16) / se_speedup_q48;
int64_t erf_val_2x = erf_q48((-z_2x_q48) / SQRT2);
est.probability_gt_double_q48 = (Q48 + erf_val_2x) >> 1;
return est;
}
/**
* @brief Print a full Bayesian cache performance report comparing two experimental conditions.
*
* Emits a structured Unicode report to @c stdout comparing the latency
* distributions of two measurement sets — one collected with the hot-words
* cache enabled, the other collected without (baseline). The report is divided
* into five sections:
*
* 1. **WITH CACHE ENABLED** — posterior mean, stddev, and 95%/99% credible
* intervals for cache-hits and bucket-searches separately, via
* @c hotwords_posterior_cache_hits() and @c hotwords_posterior_bucket_searches().
*
* 2. **WITHOUT CACHE (Baseline)** — posterior mean, stddev, and 95% credible
* interval for all lookups (treated as bucket searches). If no bucket data
* is available in @p stats_without_cache, the bucket-search mean from the
* with-cache run is used as an approximation.
*
* 3. **SPEEDUP ESTIMATE (Bucket vs Cache)** — @c hotwords_speedup_estimate()
* point estimate, 95% credible interval, P(>1.1×), and P(>2.0×).
*
* 4. **CACHE EFFECTIVENESS** — total lookups, cache-hit rate, bucket-hit rate,
* and miss rate as percentages of total.
*
* 5. **OVERALL PERFORMANCE** — weighted-average latency with cache vs. baseline
* all-bucket latency, overall speedup, and absolute time saved per lookup.
* The weighted average is: @c (N_cache×avg_cache + N_bucket×avg_bucket) /
* @c (N_cache + N_bucket).
*
* All latency values are displayed in nanoseconds (floating-point via
* @c q48_to_ns_double()) for readability; underlying accumulators are Q48.16
* integer throughout.
*
* No-op if either @p stats_with_cache or @p stats_without_cache is NULL.
*
* @param stats_with_cache Statistics from a run with cache enabled.
* @param stats_without_cache Statistics from a baseline run without cache.
*/
void hotwords_bayesian_report(const HotwordsStats *stats_with_cache,
const HotwordsStats *stats_without_cache) {
if (!stats_with_cache || !stats_without_cache) return;
printf("\n╔════════════════════════════════════════════════════════════════╗\n");
printf("║ Bayesian Performance Analysis: Cache Impact ║\n");
printf("╚════════════════════════════════════════════════════════════════╝\n\n");
/* WITH CACHE statistics */
BayesianLatencyPosterior cache_with = hotwords_posterior_cache_hits(stats_with_cache);
BayesianLatencyPosterior bucket_with = hotwords_posterior_bucket_searches(stats_with_cache);
printf("WITH CACHE ENABLED:\n");
printf("─────────────────────────────────────────────────────────────────\n");
printf("Cache Hits (N=%lu):\n", cache_with.sample_count);
printf(" Mean: %.2f ns\n", q48_to_ns_double(cache_with.mean_ns_q48));
printf(" Std Dev: %.2f ns\n", q48_to_ns_double(cache_with.stddev_ns_q48));
printf(" 95%% Credible: [%.2f, %.2f] ns\n", q48_to_ns_double(cache_with.credible_lower_95), q48_to_ns_double(cache_with.credible_upper_95));
printf(" 99%% Credible: [%.2f, %.2f] ns\n\n", q48_to_ns_double(cache_with.credible_lower_99), q48_to_ns_double(cache_with.credible_upper_99));
printf("Bucket Searches (N=%lu):\n", bucket_with.sample_count);
printf(" Mean: %.2f ns\n", q48_to_ns_double(bucket_with.mean_ns_q48));
printf(" Std Dev: %.2f ns\n", q48_to_ns_double(bucket_with.stddev_ns_q48));
printf(" 95%% Credible: [%.2f, %.2f] ns\n", q48_to_ns_double(bucket_with.credible_lower_95), q48_to_ns_double(bucket_with.credible_upper_95));
printf(" 99%% Credible: [%.2f, %.2f] ns\n\n", q48_to_ns_double(bucket_with.credible_lower_99), q48_to_ns_double(bucket_with.credible_upper_99));
/* WITHOUT CACHE statistics */
printf("WITHOUT CACHE (Baseline):\n");
printf("─────────────────────────────────────────────────────────────────\n");
printf("All Lookups (N=%lu):\n", stats_without_cache->cache_hits + stats_without_cache->bucket_hits);
/* Estimate average latency without cache (all go to bucket) */
BayesianLatencyPosterior all_lookups_baseline = hotwords_posterior_bucket_searches(stats_without_cache);
if (all_lookups_baseline.sample_count == 0) {
/* Fallback if bucket data not available */
all_lookups_baseline.mean_ns_q48 = bucket_with.mean_ns_q48;
}
printf(" Mean: %.2f ns\n", q48_to_ns_double(all_lookups_baseline.mean_ns_q48));
printf(" Std Dev: %.2f ns\n", q48_to_ns_double(all_lookups_baseline.stddev_ns_q48));
printf(" 95%% Credible: [%.2f, %.2f] ns\n\n", q48_to_ns_double(all_lookups_baseline.credible_lower_95), q48_to_ns_double(all_lookups_baseline.credible_upper_95));
/* Speedup Analysis */
struct SpeedupEstimate speedup = hotwords_speedup_estimate(stats_with_cache);
printf("SPEEDUP ESTIMATE (Bucket vs Cache):\n");
printf("─────────────────────────────────────────────────────────────────\n");
printf(" Point Estimate: %.2f×\n", q48_to_ns_double(speedup.speedup_factor_q48));
printf(" 95%% Credible Interval: [%.2f×, %.2f×]\n", q48_to_ns_double(speedup.credible_lower_95_q48), q48_to_ns_double(speedup.credible_upper_95_q48));
printf(" P(Speedup > 1.1×): %.1f%% (credible improvement exists)\n", q48_to_ns_double(speedup.probability_gt_10pct_q48) * 100.0);
printf(" P(Speedup > 2.0×): %.1f%% (significant speedup)\n\n", q48_to_ns_double(speedup.probability_gt_double_q48) * 100.0);
/* Hit Rate Analysis */
uint64_t total_lookups_with = stats_with_cache->cache_hits + stats_with_cache->bucket_hits + stats_with_cache->misses;
double cache_hit_rate = (total_lookups_with > 0) ? (100.0 * stats_with_cache->cache_hits / total_lookups_with) : 0.0;
printf("CACHE EFFECTIVENESS:\n");
printf("─────────────────────────────────────────────────────────────────\n");
printf(" Total Lookups: %lu\n", total_lookups_with);
printf(" Cache Hits: %lu (%.1f%%)\n", stats_with_cache->cache_hits, cache_hit_rate);
printf(" Bucket Hits: %lu (%.1f%%)\n", stats_with_cache->bucket_hits,
100.0 * stats_with_cache->bucket_hits / total_lookups_with);
printf(" Misses: %lu (%.1f%%)\n\n", stats_with_cache->misses,
100.0 * stats_with_cache->misses / total_lookups_with);
/* Overall Performance */
double mixed_avg_latency = ((double)stats_with_cache->cache_hits * q48_to_ns_double(cache_with.mean_ns_q48) +
(double)stats_with_cache->bucket_hits * q48_to_ns_double(bucket_with.mean_ns_q48)) /
(stats_with_cache->cache_hits + stats_with_cache->bucket_hits);
double baseline_avg_latency = q48_to_ns_double(all_lookups_baseline.mean_ns_q48);
double overall_speedup = baseline_avg_latency / mixed_avg_latency;
printf("OVERALL PERFORMANCE (WITH vs WITHOUT CACHE):\n");
printf("─────────────────────────────────────────────────────────────────\n");
printf(" Baseline (all bucket): %.2f ns/lookup\n", baseline_avg_latency);
printf(" With cache: %.2f ns/lookup\n", mixed_avg_latency);
printf(" Overall Speedup: %.2f×\n", overall_speedup);
printf(" Time Saved per Lookup: %.2f ns (%.1f%%)\n\n", baseline_avg_latency - mixed_avg_latency,
100.0 * (baseline_avg_latency - mixed_avg_latency) / baseline_avg_latency);
printf("╚════════════════════════════════════════════════════════════════╝\n\n");
}
+387
View File
@@ -0,0 +1,387 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
*** StarForth ***
physics_metadata.c - Phase 1 physics metadata helpers
Captures thermodynamic-style signals for dictionary entries without
introducing heavy runtime cost. Future phases can build on these helpers
to drive physics-aware scheduling and storage placement.
*/
#include "../include/physics_metadata.h"
#include "../include/ssm_jacquard.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* L8 FINAL INTEGRATION: L1 heat tracking is always-on (internal physics signal) */
/**
* @brief Clamp a uint64_t value to the uint32_t range.
*
* @param value Input value to clamp
* @return @c UINT32_MAX if @c value > UINT32_MAX, else @c (uint32_t)value
*/
static uint32_t clamp_u32(uint64_t value) {
return (value > UINT32_MAX) ? UINT32_MAX : (uint32_t) value;
}
/**
* @brief Derive a smoothed Q8 temperature from raw execution heat.
*
* Converts @c execution_heat to Q8 by left-shifting 8 bits, clamps to
* UINT16_MAX, then applies a 1/4 EMA blend with @c prior_q8 to suppress
* jitter: result = (3*prior + target) / 4. Returns 0 for non-positive heat.
*
* @param execution_heat Raw execution heat counter for the word
* @param prior_q8 Previous smoothed temperature in Q8 format
* @return Updated smoothed temperature in Q8 format
*/
static uint16_t temperature_from_execution_heat(cell_t execution_heat, uint16_t prior_q8) {
if (execution_heat <= 0) return 0;
uint64_t scaled = ((uint64_t) execution_heat) << 8; /* convert to Q8 */
uint16_t target = (scaled > UINT16_MAX) ? UINT16_MAX : (uint16_t) scaled;
/* Smooth with a simple EMA to avoid jitter */
return (uint16_t)((3u * prior_q8 + target) / 4u);
}
/**
* @brief Derive the physics state flags bitmask from a DictEntry's word flags.
*
* Maps @c WORD_IMMEDIATE → @c PHYSICS_STATE_IMMEDIATE,
* @c WORD_PINNED → @c PHYSICS_STATE_PINNED,
* @c WORD_HIDDEN → @c PHYSICS_STATE_HIDDEN, and
* @c WORD_COMPILED → @c PHYSICS_STATE_COMPILED.
*
* @param entry DictEntry to inspect (NULL returns 0)
* @return Bitmask of @c PHYSICS_STATE_* flags
*/
static uint8_t derive_state_flags(const DictEntry *entry) {
if (!entry) return 0;
uint8_t flags = 0;
if (entry->flags & WORD_IMMEDIATE) flags |= PHYSICS_STATE_IMMEDIATE;
if (entry->flags & WORD_PINNED) flags |= PHYSICS_STATE_PINNED;
if (entry->flags & WORD_HIDDEN) flags |= PHYSICS_STATE_HIDDEN;
if (entry->flags & WORD_COMPILED) flags |= PHYSICS_STATE_COMPILED;
return flags;
}
/**
* @brief Initialise physics metadata for a newly registered DictEntry.
*
* Zeroes all @c DictPhysics fields, sets @c mass_bytes to @c header_bytes
* (the byte size of the entry header), and derives initial @c state_flags
* from the entry's word flags. Called from @c register_word() immediately
* after the entry is linked into the dictionary.
*
* @param entry DictEntry to initialise
* @param header_bytes Byte size of the entry's header region (for mass)
*/
void physics_metadata_init(DictEntry *entry, uint32_t header_bytes) {
if (!entry) return;
entry->physics.temperature_q8 = 0;
entry->physics.last_active_ns = 0;
entry->physics.last_decay_ns = 0;
entry->physics.last_decay_tick = 0;
entry->physics.mass_bytes = header_bytes;
entry->physics.avg_latency_ns = 0;
entry->physics.state_flags = derive_state_flags(entry);
entry->physics.acl_hint = 0;
entry->physics.pubsub_mask = 0;
}
/**
* @brief Update the mass (byte footprint) of a DictEntry's physics metadata.
*
* Called after a colon definition or CREATE body is fully compiled so the
* mass reflects the total compiled size, not just the header.
*
* @param entry DictEntry to update
* @param total_bytes Total compiled byte size (header + body)
*/
void physics_metadata_set_mass(DictEntry *entry, uint32_t total_bytes) {
if (!entry) return;
entry->physics.mass_bytes = total_bytes;
}
/* ============================================================================
* INTENT: Update word's physics metadata after execution
* FL1: Heat accumulation feedback loop - converts raw execution_heat counter
* to smoothed temperature_q8 metric and updates activity timestamps
* WHY: Atomic heat increment (FL1) happens in execution loop; this function
* derives secondary metrics (temperature, timestamps) for observability
* ============================================================================ */
/**
* @brief Update physics metadata after a word execution completes.
*
* Derives and stores a new smoothed @c temperature_q8 from @c execution_heat,
* and updates both @c last_active_ns and @c last_decay_ns to @c now_ns.
* Called by @c physics_post_execute() on the post-execution path.
*
* @param entry DictEntry that was just executed
* @param execution_heat Current raw execution heat counter
* @param now_ns Current monotonic timestamp in nanoseconds
*/
void physics_metadata_touch(DictEntry *entry, cell_t execution_heat, uint64_t now_ns) {
if (!entry) return;
entry->physics.temperature_q8 = temperature_from_execution_heat(execution_heat, entry->physics.temperature_q8);
entry->physics.last_active_ns = now_ns;
entry->physics.last_decay_ns = now_ns;
}
/**
* @brief Re-derive and update @c state_flags for a DictEntry.
*
* Called after word flags change (e.g. IMMEDIATE is set during compilation)
* to keep the physics state flags in sync with the word flags.
*
* @param entry DictEntry whose state flags are to be refreshed
*/
void physics_metadata_refresh_state(DictEntry *entry) {
if (!entry) return;
entry->physics.state_flags = derive_state_flags(entry);
}
/**
* @brief Record an execution latency sample into the per-word EMA.
*
* Clamps @c sample_ns to uint32_t, then blends it into @c avg_latency_ns
* with a 1/4 EMA: new = (3*prior + sample) / 4. Seeds @c avg_latency_ns
* directly from the first sample.
*
* @param entry DictEntry whose latency EMA is updated
* @param sample_ns Measured execution duration in nanoseconds
*/
void physics_metadata_record_latency(DictEntry *entry, uint64_t sample_ns) {
if (!entry) return;
uint32_t sample = clamp_u32(sample_ns);
uint32_t prior = entry->physics.avg_latency_ns;
if (prior == 0) {
entry->physics.avg_latency_ns = sample;
} else {
entry->physics.avg_latency_ns = (uint32_t)((3ull * prior + sample) / 4ull);
}
}
typedef struct physics_seed_definition {
const char *name;
uint16_t temperature_q8;
uint32_t avg_latency_ns;
uint8_t acl_hint;
uint8_t pubsub_mask;
} physics_seed_definition_t;
static const physics_seed_definition_t physics_seed_table[] = {
{"IF", 0x0100u, 400u, 0, 0},
{"ELSE", 0x0100u, 400u, 0, 0},
{"THEN", 0x0100u, 350u, 0, 0},
{"BEGIN", 0x0120u, 450u, 0, 0},
{"WHILE", 0x0120u, 450u, 0, 0},
{"REPEAT", 0x0120u, 450u, 0, 0},
{"DO", 0x0180u, 500u, 0, 0},
{"LOOP", 0x0180u, 500u, 0, 0},
{"+LOOP", 0x0200u, 520u, 0, 0},
{"LEAVE", 0x0200u, 520u, 0, 0},
{".", 0x0080u, 300u, 0, 0},
{"EMIT", 0x0180u, 800u, 0, 0x02u},
{"TYPE", 0x0180u, 900u, 0, 0x02u},
{"BLOCK", 0x0200u, 950u, 0, 0x04u},
{"BUFFER", 0x01C0u, 900u, 0, 0x04u},
{"SAVE-BUFFERS", 0x0260u, 1400u, 0, 0x04u},
{"FLUSH", 0x0240u, 1200u, 0, 0x04u},
{"LOAD", 0x0220u, 1100u, 0, 0x04u},
{"LIST", 0x0200u, 1000u, 0, 0x04u},
{"SAVE-SYSTEM", 0x0280u, 2000u, 0x80u, 0x08u},
{NULL, 0, 0, 0, 0}
};
/**
* @brief Apply DoE-derived prior physics values to a named DictEntry.
*
* Searches the static @c physics_seed_table for an entry matching
* @c entry->name. If found, seeds @c temperature_q8, @c avg_latency_ns,
* @c acl_hint, and @c pubsub_mask with values derived from the 2^7
* factorial DoE analysis. Only non-zero seed fields overwrite the current
* value. Called from @c register_word() for built-in primitives.
*
* @param entry DictEntry to seed (no-op if NULL or no matching table entry)
*/
void physics_metadata_apply_seed(DictEntry *entry) {
if (!entry || !entry->name_len) return;
for (const physics_seed_definition_t *seed = physics_seed_table; seed->name; ++seed) {
if ((int) strlen(seed->name) != entry->name_len) continue;
if (strncmp(seed->name, entry->name, entry->name_len) != 0) continue;
if (seed->temperature_q8) entry->physics.temperature_q8 = seed->temperature_q8;
if (seed->avg_latency_ns) entry->physics.avg_latency_ns = seed->avg_latency_ns;
if (seed->acl_hint) entry->physics.acl_hint = seed->acl_hint;
if (seed->pubsub_mask) entry->physics.pubsub_mask = seed->pubsub_mask;
break;
}
}
/*
* ============================================================================
* Phase 2: Linear Decay Mechanism
* ============================================================================
*
* Reduces execution_heat over time to model:
* - OS context switches (old task's heat becomes stale)
* - Temporal locality (recent access patterns > distant past)
* - Dissipation (physical analogy: thermal equilibrium)
*
* Mathematical Model (Discrete Linear):
* H(t) = max(0, H_0 - d*t)
*
* Where:
* H(t) = heat at time t
* H_0 = initial heat
* d = decay rate (heat units per heartbeat tick)
* t = heartbeat ticks elapsed since last execution -- never wall-clock
* time; see physics_metadata_apply_linear_decay()'s doc comment
*
* Frozen words (WORD_FROZEN flag) are exempt from decay.
* ============================================================================
*/
/**
* @brief Apply Loop #3 linear heat decay to a DictEntry.
*
* Implements the thermodynamic dissipation model: H(t) = max(0, H₀ - d*t),
* where t is measured in heartbeat ticks (@c vm->heartbeat.tick_count),
* never in wall-clock time. The decay amount is computed as
* (elapsed_ticks * slope_q48) >> 16 using the Q48.16 adaptive decay slope
* stored in the VM's heartbeat state. Uses a GCC CAS loop for atomic heat
* update when @c __GNUC__ is defined, falling back to a plain subtraction
* otherwise.
*
* Ticks, not nanoseconds: this entry's @c execution_heat feeds directly
* into @c dict_hash (see @c capsule_dict_hash_hook()), so any wall-clock
* dependency here makes dictionary state -- and therefore dict_hash --
* architecture-variable under QEMU/TCG's differing real emulation speed,
* or whenever real elapsed time between two touches of the same entry
* varies for reasons unrelated to execution (e.g. a blocking I/O wait
* between them). @c vm->heartbeat.tick_count only advances from
* @c vm_tick(), called synchronously from the interpreter's own dispatch
* path -- a purely execution-count-driven clock, immune to how long any
* intervening real-time work (including blocking I/O) actually took.
*
* No-op when:
* - @c WORD_FROZEN flag is set on the entry
* - @c elapsed_ticks == 0 (no heartbeat tick has fired since last decay)
* - @c vm->ssm_config has L3 disabled by the Jacquard mode selector
*
* @param entry DictEntry whose heat is decayed
* @param elapsed_ticks Heartbeat ticks elapsed since last decay application
* @param vm VM providing the adaptive decay slope and L3 gate flag
*/
void physics_metadata_apply_linear_decay(DictEntry *entry, uint64_t elapsed_ticks, VM *vm) {
if (!entry || !vm) {
return;
}
/* Frozen words do not decay */
if (entry->flags & WORD_FROZEN) {
return;
}
/* Don't bother when no tick has fired since the last application */
if (elapsed_ticks == 0) {
return;
}
/* L8 GATE: Check if L3 (linear decay) is enabled by Jacquard mode selector */
if (vm->ssm_config) {
ssm_config_t *config = (ssm_config_t*)vm->ssm_config;
if (!config->L3_linear_decay) {
return; /* L3 disabled by L8 mode selector */
}
}
/* ========================================================================
* INTENT: Calculate decay amount using adaptive Q48.16 fixed-point math
* FL1: Heat decay - counterbalances FL1 accumulation to prevent runaway
* FL3: Adaptive slope tuning - inference engine adjusts decay_slope_q48
* based on statistical analysis of heat trajectory effectiveness
* WHY: decay_slope_q48 is expressed as heat units per tick, not per
* microsecond -- the inference engine (Loop #6) adaptively
* calibrates it against whatever units it's actually applied in,
* so no conversion constant is needed here, just a consistent unit.
* ======================================================================== */
uint64_t slope_q48 = physics_decay_slope_load(vm);
uint64_t decay_amount_raw = (elapsed_ticks * slope_q48) >> 16;
/* Clamp to cell_t range (avoid overflow) */
cell_t decay_amount = (decay_amount_raw > (uint64_t)0x7FFFFFFFFFFFFFFF)
? 0x7FFFFFFFFFFFFFFF
: (cell_t)decay_amount_raw;
if (decay_amount == 0) {
return;
}
#if defined(__GNUC__)
cell_t old_heat;
cell_t new_heat;
do {
old_heat = __atomic_load_n(&entry->execution_heat, __ATOMIC_RELAXED);
if (old_heat == 0) {
return;
}
new_heat = (decay_amount >= old_heat) ? 0 : (old_heat - decay_amount);
} while (!__atomic_compare_exchange_n(&entry->execution_heat,
&old_heat,
new_heat,
0,
__ATOMIC_RELAXED,
__ATOMIC_RELAXED));
#else
if (decay_amount >= entry->execution_heat) {
entry->execution_heat = 0;
} else {
entry->execution_heat -= decay_amount;
}
#endif
}
+754
View File
@@ -0,0 +1,754 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
*** StarForth ***
physics_pipelining_metrics.c - Word transition tracking implementation
Tracks word-to-word transitions to support speculative execution/pipelining.
Uses only fixed-point Q48.16 arithmetic (no floating-point).
*/
#include "physics_pipelining_metrics.h"
#include "vm.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* ============================================================================
* Q48.16 Fixed-Point Arithmetic Helpers
* ============================================================================
*/
/**
* @brief Convert a nanosecond count to Q48.16 fixed-point.
*
* Shifts @p ns left by 16 bits to produce a Q48.16 value where the integer
* part represents nanoseconds (1.0 ns = 65536 in Q48.16). Used to convert
* raw platform nanosecond timestamps into the common Q48.16 format before
* accumulating into @c prefetch_latency_saved_q48 and
* @c misprediction_cost_q48.
*
* @param ns Integer nanosecond value to convert.
* @return @p ns in Q48.16 format (@p ns × 65536).
*/
static inline int64_t ns_to_q48(int64_t ns) {
return ns << 16;
}
/**
* @brief Convert a Q48.16 fixed-point value to integer nanoseconds (truncated).
*
* Shifts @p q48 right by 16 bits, discarding the fractional part. The result
* is the integer nanosecond count represented by the Q48.16 value. Used in
* @c transition_metrics_stats_string() to convert the accumulated
* @c prefetch_latency_saved_q48 and @c misprediction_cost_q48 back to
* human-readable nanosecond integers for the stats report string.
*
* @param q48 Value in Q48.16 format.
* @return Integer nanosecond equivalent (truncated, not rounded).
*/
static inline int64_t q48_to_ns(int64_t q48) {
return q48 >> 16;
}
/**
* @brief Divide a Q48.16 value by a @c uint64_t, returning Q48.16.
*
* Computes @p a_q48 / @p b using integer division. The result preserves the
* Q48.16 scaling of @p a_q48 because @p b is dimensionless. Guards against
* divide-by-zero by returning 0 when @p b == 0. Used by
* @c transition_metrics_get_probability_q48() to compute the per-word
* transition probability:
* @code
* P(next = w) = (count_q48 << 16) / total_transitions
* @endcode
* where @c count_q48 = (uint64_t)count << 16 and @c total_transitions is the
* denominator @p b.
*
* @param a_q48 Numerator in Q48.16 format.
* @param b Denominator as a plain @c uint64_t (dimensionless count).
* @return @p a_q48 / @p b in Q48.16; 0 if @p b == 0.
*/
static inline int64_t q48_div_u64(int64_t a_q48, uint64_t b) {
if (b == 0) return 0;
return a_q48 / (int64_t)b;
}
/* ============================================================================
* Transition Metrics Implementation
* ============================================================================
*/
/**
* @brief Initialise a @c WordTransitionMetrics structure to its empty, ready-to-record state.
*
* Sets all counter and accumulator fields to zero and performs the following
* allocations:
*
* - @c metrics->transition_heat is left NULL — it is lazily allocated by the
* first call to @c transition_metrics_record() to avoid the @c dict_size
* parameter being required at construction time.
*
* - If @c TRANSITION_WINDOW_SIZE > 0, allocates @c metrics->context_window as
* a zero-filled array of @c TRANSITION_WINDOW_SIZE @c uint32_t word IDs and
* sets @c metrics->actual_window_size accordingly. If the allocation fails,
* @c actual_window_size is set to 0 and context tracking degrades gracefully
* (all context functions become no-ops).
*
* - @c metrics->context_transitions is left NULL — it is lazily allocated when
* Phase 2 context-pattern analysis is implemented.
*
* Returns 0 on success or −1 if @p metrics is NULL.
*
* @param metrics The @c WordTransitionMetrics to initialise; must not be NULL.
* @return 0 on success; −1 if @p metrics is NULL.
*/
int transition_metrics_init(WordTransitionMetrics *metrics) {
if (!metrics) return -1;
metrics->transition_heat = NULL; /* Lazy allocation on first use */
metrics->total_transitions = 0;
metrics->prefetch_attempts = 0;
metrics->prefetch_hits = 0;
metrics->prefetch_misses = 0;
metrics->prefetch_latency_saved_q48 = 0;
metrics->misprediction_cost_q48 = 0;
metrics->max_transition_probability_q48 = 0;
metrics->most_likely_next_word_id = 0;
/* Phase 1 Extension: Initialize context window (window size = 2 by default) */
if (TRANSITION_WINDOW_SIZE > 0) {
metrics->context_window = (uint32_t *)calloc(TRANSITION_WINDOW_SIZE, sizeof(uint32_t));
if (metrics->context_window) {
metrics->actual_window_size = TRANSITION_WINDOW_SIZE;
metrics->context_window_pos = 0;
} else {
metrics->actual_window_size = 0; /* Degraded: no context tracking */
}
} else {
metrics->actual_window_size = 0;
}
metrics->context_transitions = NULL; /* Lazy allocation on first context transition */
metrics->total_context_transitions = 0;
return 0;
}
/**
* @brief Record one word-to-word transition in the transition heat table.
*
* Called from the interpreter loop each time a word executes and there is a
* known previous word. Increments @c metrics->transition_heat[next_word_id]
* by one and @c metrics->total_transitions by one.
*
* **Lazy allocation:** The @c transition_heat array is not allocated at
* @c transition_metrics_init() time; on the first call when
* @c metrics->transition_heat == NULL, it is allocated as a zero-filled
* array of @p dict_size @c uint64_t counters. If the allocation fails −1 is
* returned and the transition is silently dropped.
*
* If @p next_word_id ≥ @p dict_size the call is rejected with −1 to prevent
* an out-of-bounds write into @c transition_heat (which was allocated with
* size @p dict_size).
*
* The accumulated @c transition_heat[] table is consumed by
* @c transition_metrics_get_probability_q48() and
* @c transition_metrics_update_cache() to determine the most-likely next word
* for speculative prefetch.
*
* @param metrics The metrics structure to update.
* @param next_word_id Zero-based word ID of the word that followed the
* current word; must be < @p dict_size.
* @param dict_size Current dictionary size, used both as the bounds
* check for @p next_word_id and as the allocation size
* for the lazy-allocated @c transition_heat array.
* @return 0 on success; −1 on NULL @p metrics, out-of-bounds
* @p next_word_id, or allocation failure.
*/
int transition_metrics_record(WordTransitionMetrics *metrics, uint32_t next_word_id, uint32_t dict_size) {
if (!metrics || next_word_id >= dict_size) {
return -1;
}
/* Lazy allocation: only allocate transition_heat array when first transition occurs */
if (!metrics->transition_heat) {
metrics->transition_heat = (uint64_t *)calloc(dict_size, sizeof(uint64_t));
if (!metrics->transition_heat) {
return -1; /* Memory allocation failure */
}
}
/* Record the transition */
metrics->transition_heat[next_word_id]++;
metrics->total_transitions++;
return 0;
}
/**
* @brief Return the transition probability to @p target_word_id in Q48.16 format.
*
* Computes P(next = @p target_word_id | current word) as a Q48.16 fixed-point
* ratio:
* @code
* P = (transition_heat[target_word_id] << 16) / total_transitions
* @endcode
* using @c q48_div_u64() to guard against divide-by-zero.
*
* Returns 0 if:
* - @p metrics is NULL.
* - @c metrics->total_transitions == 0 (no transitions recorded yet).
* - @c metrics->transition_heat is NULL (lazy allocation has not occurred).
*
* The caller is responsible for ensuring @p target_word_id is within the
* bounds of the allocated @c transition_heat array; this function does not
* re-check bounds after verifying the NULL guard above (bounds were enforced
* during @c transition_metrics_record()).
*
* Consumed by @c transition_metrics_update_cache() (scans all words to find
* the maximum) and @c transition_metrics_should_speculate() (checks whether
* a specific word's probability exceeds @c SPECULATION_THRESHOLD_Q48).
*
* @param metrics The metrics structure to query.
* @param target_word_id Zero-based word ID whose transition probability is
* requested; must be within the dict_size used at record time.
* @return P(next = @p target_word_id) in Q48.16; 0 if unavailable.
*/
int64_t transition_metrics_get_probability_q48(const WordTransitionMetrics *metrics, uint32_t target_word_id) {
if (!metrics || metrics->total_transitions == 0) {
return 0;
}
/* Safety: Bounds check on target_word_id to prevent out-of-bounds read
* Note: transition_heat is allocated with dict_size passed to transition_metrics_record()
* Callers must ensure target_word_id < dict_size used during allocation */
if (!metrics->transition_heat) {
return 0;
}
uint64_t count = metrics->transition_heat[target_word_id];
/* Probability = count / total_transitions, in Q48.16 format */
/* = (count << 16) / total_transitions */
int64_t count_q48 = (int64_t)count << 16;
return q48_div_u64(count_q48, metrics->total_transitions);
}
/**
* @brief Scan the transition heat table and cache the highest-probability next word.
*
* Iterates over all @p dict_size entries in @c metrics->transition_heat[],
* calling @c transition_metrics_get_probability_q48() for each non-zero entry
* and tracking the maximum. After the scan:
* - @c metrics->max_transition_probability_q48 holds the peak probability (Q48.16).
* - @c metrics->most_likely_next_word_id holds the word ID of that peak.
*
* These two cached fields allow @c transition_metrics_should_speculate() to
* make a prefetch decision in O(1) without re-scanning the full table on every
* word dispatch.
*
* If @p metrics, @c transition_heat, or @c total_transitions are not valid,
* both fields are reset to 0 and the function returns immediately.
*
* Called after each @c transition_metrics_record() in the high-frequency
* interpreter path, so the implementation must be fast. The full @p dict_size
* scan runs in O(dict_size) and skips zero-count entries early.
*
* @param metrics The metrics structure to update.
* @param dict_size Number of entries in @c metrics->transition_heat[];
* must match the size used at allocation time.
*/
void transition_metrics_update_cache(WordTransitionMetrics *metrics, uint32_t dict_size) {
if (!metrics || !metrics->transition_heat || metrics->total_transitions == 0) {
metrics->max_transition_probability_q48 = 0;
metrics->most_likely_next_word_id = 0;
return;
}
int64_t max_prob_q48 = 0;
uint32_t max_idx = 0;
/* Find word with highest transition count */
for (uint32_t i = 0; i < dict_size; i++) {
if (metrics->transition_heat[i] > 0) {
int64_t prob_q48 = transition_metrics_get_probability_q48(metrics, i);
if (prob_q48 > max_prob_q48) {
max_prob_q48 = prob_q48;
max_idx = i;
}
}
}
metrics->max_transition_probability_q48 = max_prob_q48;
metrics->most_likely_next_word_id = max_idx;
}
/**
* @brief Record a successful speculative prefetch and the latency it saved.
*
* Increments @c metrics->prefetch_hits by one and accumulates @p latency_saved_ns
* (converted to Q48.16 via @c ns_to_q48()) into
* @c metrics->prefetch_latency_saved_q48. The accumulated total is reported by
* @c transition_metrics_stats_string() as the aggregate latency benefit of
* correct speculation.
*
* Called by the speculative-execution path when a prefetched word matches the
* word that was actually executed next.
*
* @param metrics The metrics structure to update; no-op if NULL.
* @param latency_saved_ns The number of nanoseconds saved by avoiding a
* full dictionary lookup on this particular hit.
*/
void transition_metrics_record_prefetch_hit(WordTransitionMetrics *metrics, int64_t latency_saved_ns) {
if (!metrics) return;
metrics->prefetch_hits++;
metrics->prefetch_latency_saved_q48 += ns_to_q48(latency_saved_ns);
}
/**
* @brief Record a failed speculative prefetch and its recovery overhead.
*
* Increments @c metrics->prefetch_misses by one and accumulates
* @p recovery_cost_ns (converted to Q48.16 via @c ns_to_q48()) into
* @c metrics->misprediction_cost_q48. The accumulated total is the aggregate
* penalty paid for incorrect speculation and is reported by
* @c transition_metrics_stats_string() as "Misprediction Cost".
*
* The net benefit of the pipelining subsystem is:
* @code
* net_ns = prefetch_latency_saved_q48 - misprediction_cost_q48
* @endcode
* A positive net indicates that speculation is profitable overall.
*
* Called by the speculative-execution path when the prefetched word does not
* match the word that was actually executed next (branch misprediction analogue).
*
* @param metrics The metrics structure to update; no-op if NULL.
* @param recovery_cost_ns The number of nanoseconds spent unwinding the
* incorrect speculation (cache flush, state restore).
*/
void transition_metrics_record_prefetch_miss(WordTransitionMetrics *metrics, int64_t recovery_cost_ns) {
if (!metrics) return;
metrics->prefetch_misses++;
metrics->misprediction_cost_q48 += ns_to_q48(recovery_cost_ns);
}
/**
* @brief Decide whether to speculatively prefetch @p target_word_id.
*
* Applies a two-gate policy before authorising speculation:
*
* 1. **Minimum sample gate:** @c metrics->total_transitions must be ≥
* @c MIN_SAMPLES_FOR_SPECULATION. Below this threshold the transition
* probabilities are statistically unreliable — speculation would be
* indistinguishable from guessing.
*
* 2. **Probability threshold gate:** the Q48.16 probability returned by
* @c transition_metrics_get_probability_q48(metrics, @p target_word_id)
* must be ≥ @c SPECULATION_THRESHOLD_Q48. Words with very low conditional
* probability of following the current word are not worth prefetching.
*
* Both gates must pass for the function to return 1. A Phase 4 ROI check
* (comparing expected latency savings against expected misprediction cost) is
* planned but not yet implemented; a TODO comment marks the extension point.
*
* Returns 0 if @p metrics or @c transition_heat are NULL, or if either gate
* fails.
*
* @param metrics The metrics structure to consult.
* @param target_word_id Zero-based ID of the candidate next word.
* @return 1 if speculation is recommended; 0 otherwise.
*/
int transition_metrics_should_speculate(const WordTransitionMetrics *metrics, uint32_t target_word_id) {
if (!metrics || !metrics->transition_heat) {
return 0; /* No data yet */
}
/* Check: do we have enough samples? */
if (metrics->total_transitions < MIN_SAMPLES_FOR_SPECULATION) {
return 0; /* Not enough data */
}
/* Get probability of this word following */
int64_t prob_q48 = transition_metrics_get_probability_q48(metrics, target_word_id);
/* Check: does probability exceed threshold? */
if (prob_q48 < SPECULATION_THRESHOLD_Q48) {
return 0; /* Probability too low */
}
/* TODO: Phase 4 - Add ROI check here */
/* For now, threshold check is sufficient */
return 1; /* Should speculate */
}
/**
* @brief Allocate and return a human-readable transition metrics summary string.
*
* Allocates a 2048-byte buffer and fills it with a multi-line report:
* - Total transitions recorded.
* - Prefetch attempts, hits (with hit-rate %), and misses.
* - Aggregate latency saved by correct speculation (from
* @c prefetch_latency_saved_q48, converted to integer ns via @c q48_to_ns()).
* - Aggregate misprediction cost (from @c misprediction_cost_q48).
* - Net benefit (saved − cost) in nanoseconds.
* - The most-likely next word ID and its raw Q48.16 probability.
*
* The caller is responsible for calling @c free() on the returned pointer.
* Returns NULL if @p metrics is NULL or @c malloc() fails.
*
* @param metrics The metrics structure to summarise.
* @return Heap-allocated NUL-terminated stats string; caller must @c free().
* NULL on allocation failure or NULL @p metrics.
*/
char *transition_metrics_stats_string(const WordTransitionMetrics *metrics) {
if (!metrics) return NULL;
/* Allocate buffer for stats string */
char *buf = (char *)malloc(2048);
if (!buf) return NULL;
int saved_ns = q48_to_ns(metrics->prefetch_latency_saved_q48);
int cost_ns = q48_to_ns(metrics->misprediction_cost_q48);
int net_ns = saved_ns - cost_ns;
/* Calculate hit rate */
uint64_t total_speculations = metrics->prefetch_attempts;
double hit_rate = (total_speculations > 0) ? (100.0 * metrics->prefetch_hits / total_speculations) : 0.0;
snprintf(buf, 2048,
"Transition Statistics:\n"
" Total Transitions: %lu\n"
" Prefetch Attempts: %lu\n"
" Prefetch Hits: %lu (%.1f%% hit rate)\n"
" Prefetch Misses: %lu\n"
" Latency Saved: %d ns\n"
" Misprediction Cost: %d ns\n"
" Net Benefit: %d ns\n"
" Most Likely Next: word[%u] (P=0x%lx Q48.16)\n",
metrics->total_transitions,
metrics->prefetch_attempts,
metrics->prefetch_hits, hit_rate,
metrics->prefetch_misses,
saved_ns,
cost_ns,
net_ns,
metrics->most_likely_next_word_id,
metrics->max_transition_probability_q48);
return buf;
}
/**
* @brief Reset a @c WordTransitionMetrics to empty without freeing the context window.
*
* Frees and NULLs @c metrics->transition_heat (so the next
* @c transition_metrics_record() will re-allocate it), then zeroes all
* counter and accumulator fields. Also zeroes the context-window array
* (if allocated) and resets @c context_window_pos to 0, and frees and NULLs
* @c context_transitions. @c actual_window_size and the @c context_window
* pointer itself are preserved — the window stays allocated and ready for
* reuse.
*
* Intended for DoE benchmark runs that need a clean statistical baseline
* between treatment cells without the overhead of full teardown and
* reinitialisation. Cheaper than @c transition_metrics_cleanup() +
* @c transition_metrics_init() because it avoids reallocating the context
* window.
*
* @param metrics The metrics structure to reset; no-op if NULL.
*/
void transition_metrics_reset(WordTransitionMetrics *metrics) {
if (!metrics) return;
if (metrics->transition_heat) {
free(metrics->transition_heat);
metrics->transition_heat = NULL;
}
metrics->total_transitions = 0;
metrics->prefetch_attempts = 0;
metrics->prefetch_hits = 0;
metrics->prefetch_misses = 0;
metrics->prefetch_latency_saved_q48 = 0;
metrics->misprediction_cost_q48 = 0;
metrics->max_transition_probability_q48 = 0;
metrics->most_likely_next_word_id = 0;
/* Phase 1 Extension: Reset context window */
if (metrics->context_window) {
memset(metrics->context_window, 0, metrics->actual_window_size * sizeof(uint32_t));
metrics->context_window_pos = 0;
}
if (metrics->context_transitions) {
free(metrics->context_transitions);
metrics->context_transitions = NULL;
}
metrics->total_context_transitions = 0;
}
/**
* @brief Release all heap memory held by a @c WordTransitionMetrics.
*
* Frees and NULLs:
* - @c metrics->transition_heat — the per-word transition count array.
* - @c metrics->context_window — the sliding context-window array.
* - @c metrics->context_transitions — the Phase 2 sparse hash table (if allocated).
*
* Does NOT zero the struct fields beyond NULLing the freed pointers. The
* caller must not use @p metrics after this call without a subsequent
* @c transition_metrics_init(). Contrast with @c transition_metrics_reset(),
* which keeps the context window allocated for reuse.
*
* No-op if @p metrics is NULL.
*
* @param metrics The metrics structure to clean up; no-op if NULL.
*/
void transition_metrics_cleanup(WordTransitionMetrics *metrics) {
if (!metrics) return;
if (metrics->transition_heat) {
free(metrics->transition_heat);
metrics->transition_heat = NULL;
}
/* Phase 1 Extension: Clean up context window */
if (metrics->context_window) {
free(metrics->context_window);
metrics->context_window = NULL;
}
if (metrics->context_transitions) {
free(metrics->context_transitions);
metrics->context_transitions = NULL;
}
}
/* ============================================================================
* Context-Aware Transition Functions (Phase 1 Extension)
* ============================================================================
*
* Phase 1: Collect data only (these functions are stubs)
* Phase 2: Implement analysis logic
* Phase 3: Use for adaptive decisions
*/
/**
* @brief Slide the context window forward by one word, appending @p word_id.
*
* Implements a fixed-length sliding window over the recent word-ID history.
* Each call shifts all existing entries one position to the left (dropping
* the oldest), then writes @p word_id into the last slot:
* @code
* context_window[0..n-2] = context_window[1..n-1]
* context_window[n-1] = word_id
* @endcode
* This produces a FIFO queue of the last @c actual_window_size word IDs
* that can be passed to @c transition_metrics_record_context() for
* context-sensitive transition tracking (Phase 2).
*
* Returns −1 and is a no-op if:
* - @p metrics is NULL.
* - @c context_window is NULL (allocation at init failed — degraded mode).
* - @c actual_window_size == 0.
*
* @param metrics The metrics structure whose context window is updated.
* @param word_id The word ID just executed; appended to the right of the window.
* @return 0 on success; −1 if the context window is unavailable.
*/
int transition_metrics_update_context_window(WordTransitionMetrics *metrics, uint32_t word_id) {
if (!metrics || !metrics->context_window || metrics->actual_window_size == 0) {
return -1; /* Context tracking not initialized */
}
/* Slide window forward: shift elements left, add new word on right */
for (uint32_t i = 0; i < metrics->actual_window_size - 1; i++) {
metrics->context_window[i] = metrics->context_window[i + 1];
}
/* Add new word at end of window */
metrics->context_window[metrics->actual_window_size - 1] = word_id;
return 0;
}
/**
* @brief Record a context-conditioned transition (Phase 1 collection stub).
*
* Accepts a @p context window of @p window_size preceding word IDs and the
* @p next_word_id that followed. In Phase 1 only the total count is tracked:
* @c metrics->total_context_transitions is incremented by one, providing a
* baseline count that Phase 2 analysis can use to size its hash tables.
*
* **Phase 2 TODO:** Build a sparse hash table keyed on the context pattern
* @c (context[0], …, context[window_size-1]) to record per-context next-word
* distributions. The Phase 2 analysis will feed
* @c transition_metrics_binary_chop_suggest_window() with accuracy data to
* tune the optimal window size.
*
* Returns −1 (no-op) if @p metrics or @p context are NULL, @p window_size is
* 0, or @p next_word_id ≥ @p dict_size.
*
* @param metrics The metrics structure to update.
* @param context Array of @p window_size word IDs preceding @p next_word_id.
* @param window_size Number of entries in @p context; must be > 0.
* @param next_word_id Word ID that followed the context sequence.
* @param dict_size Current dictionary size; used for bounds checking
* @p next_word_id.
* @return 0 on success; −1 on invalid arguments.
*/
int transition_metrics_record_context(WordTransitionMetrics *metrics,
const uint32_t *context,
uint32_t window_size,
uint32_t next_word_id,
uint32_t dict_size) {
if (!metrics || !context || window_size == 0 || next_word_id >= dict_size) {
return -1;
}
/* Phase 1: Just count total context transitions for validation */
metrics->total_context_transitions++;
/* TODO Phase 2: Build sparse hash table of context patterns
* For now, we collect data but don't analyze it.
* This is the foundation for Phase 2's binary chop tuning.
*/
return 0;
}
/**
* @brief Allocate and return a context-window status report string (Phase 1 stub).
*
* Allocates a 1024-byte buffer and fills it with a brief Phase 1 collection
* status message:
* - @c actual_window_size — the active window width (0 if allocation failed).
* - @c total_context_transitions — the number of context transitions recorded.
* - A "[Phase 1 Collection - Phase 2 Analysis Pending]" status line.
*
* Phase 2 will replace this stub with a real accuracy report showing per-window-size
* prediction accuracy derived from the sparse context-pattern hash table. The
* accuracy data will feed @c transition_metrics_binary_chop_suggest_window()
* to converge on the optimal context window width.
*
* The caller is responsible for calling @c free() on the returned pointer.
* Returns NULL if @p metrics is NULL or @c malloc() fails.
*
* @param metrics The metrics structure to report on.
* @return Heap-allocated NUL-terminated string; caller must @c free().
* NULL on allocation failure or NULL @p metrics.
*/
char *transition_metrics_context_accuracy_string(const WordTransitionMetrics *metrics) {
if (!metrics) return NULL;
char *buf = (char *)malloc(1024);
if (!buf) return NULL;
/* Phase 1: Stub - just report collection status */
snprintf(buf, 1024,
"Context Metrics Status:\n"
" Actual Window Size: %u\n"
" Total Context Transitions: %lu\n"
" Status: [Phase 1 Collection - Phase 2 Analysis Pending]\n",
metrics->actual_window_size,
metrics->total_context_transitions);
return buf;
}
/**
* @brief Suggest a new context window size using binary-chop search (Phase 1 stub).
*
* In Phase 2 this function will implement a binary-chop search over window sizes
* to converge on the width that maximises context-prediction accuracy:
* - Start at window = 2.
* - If accuracy improves when doubling, try 4; if it declines, try 1.
* - Continue halving/doubling until the accuracy delta falls below a threshold.
*
* **Phase 1 stub behaviour:** Doubles the current window each call up to 8,
* then wraps back to 1:
* - @p current_window == 1 → suggests 2
* - @p current_window == 2 → suggests 4
* - @p current_window == 4 → suggests 8
* - @p current_window ≥ 8 → suggests 1 (cycle back)
* - Any other value → returns @p current_window unchanged.
*
* The @p accuracy_at_current parameter (prediction accuracy [0.0, 1.0] at the
* current window size) is accepted for API stability with the Phase 2 caller
* but is not used in the stub.
*
* @param metrics The metrics structure (unused in Phase 1 stub;
* reserved for Phase 2 accuracy history).
* @param current_window Currently active context window size.
* @param accuracy_at_current Prediction accuracy at @p current_window
* (unused in Phase 1 stub; range [0.0, 1.0]).
* @return Suggested new window size.
*/
uint32_t transition_metrics_binary_chop_suggest_window(const WordTransitionMetrics *metrics,
uint32_t current_window,
double accuracy_at_current) {
(void)metrics;
(void)accuracy_at_current;
/* Phase 1: Stub - just return doubled window size per user's request
* Phase 2 will implement actual binary chop search:
* - Start at window=2
* - If accuracy improves, try doubling (2→4)
* - If accuracy declines, try halving (4→2)
* - Converge to optimal size
*/
if (current_window >= 8) return 1; /* Maximum: back to 1 */
if (current_window == 1) return 2; /* 1 → 2 */
if (current_window == 2) return 4; /* 2 → 4 (doubling) */
if (current_window == 4) return 8; /* 4 → 8 (doubling) */
return current_window; /* Fallback */
}
+720
View File
@@ -0,0 +1,720 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
*** StarForth ***
physics_runtime.c - Phase 1 physics runtime scaffolding
Implements the host snapshot shim (POSIX) and provides a
fixed-size analytics heap compatible with the HOLA shared-memory layout.
The implementation favours predictable costs and clear extension points for
future governance-approved features.
*/
#include "physics_runtime.h"
#include "platform_time.h"
/* STARFORTH_MINIMAL: kernel build — skip Linux /proc runtime monitoring.
* Provide stub API so vmcore link succeeds; none of these are called. */
#if defined(STARFORTH_MINIMAL)
int physics_runtime_init(size_t analytics_heap_bytes)
{ (void)analytics_heap_bytes; return 0; }
void physics_runtime_shutdown(void) {}
int physics_analytics_publish_event(uint32_t channel, const void *payload, uint16_t payload_bytes)
{ (void)channel; (void)payload; (void)payload_bytes; return 0; }
int physics_host_snapshot(physics_host_snapshot_t *out)
{ (void)out; return -1; }
#else /* !STARFORTH_MINIMAL — full Linux/POSIX implementation below */
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
/* HISTORICAL: L4Re support has been removed as an active target.
* #ifdef __l4__
* #include <l4/sys/kip.h>
* #include <l4/re/env.h>
* #endif
*/
#include <sched.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <ctype.h>
/* ------------------------------------------------------------------------- */
/* Analytics heap internals */
/* ------------------------------------------------------------------------- */
typedef struct physics_runtime_state {
physics_analytics_heap_t heap;
int heap_ready;
uint64_t backend_seq;
} physics_runtime_state_t;
static physics_runtime_state_t g_runtime = {0};
/**
* @brief Round @c value up to the next multiple of @c alignment.
*
* @c alignment must be a power of two. Used internally to satisfy
* cache-line and struct alignment requirements in the analytics heap.
*
* @param value Value to align
* @param alignment Alignment boundary (must be power of two)
* @return Smallest multiple of @c alignment that is >= @c value
*/
static size_t align_up(size_t value, size_t alignment) {
return (value + alignment - 1u) & ~(alignment - 1u);
}
/* HISTORICAL: this whole section (through the matching "!__l4__" comment
* below) was wrapped in `#if !defined(__l4__)` -- these PSI/cgroup helpers
* are Linux-specific and were excluded from L4Re builds. L4Re support has
* been removed as an active target, so the guard (now always-true) has
* been removed too. */
/**
* @brief Parse a floating-point average field from a Linux PSI file line.
*
* Searches @c line for @c tag (e.g. @c "avg10="), reads the following
* floating-point value, and returns it scaled to millipercentage (×1000).
* Returns 0 on parse failure or negative value.
*
* @param line Raw text line from a @c /proc/pressure/ PSI file (cpu, memory, or io)
* @param tag Field prefix to locate (e.g. @c "avg10=")
* @return Scaled value in milli-units, or 0 on failure
*/
static uint32_t parse_avg_milli(const char *line, const char *tag) {
const char *p = strstr(line, tag);
if (!p) return 0;
p += strlen(tag);
errno = 0;
char *end = NULL;
double value = strtod(p, &end);
if (errno != 0 || end == p || value < 0.0) {
return 0;
}
double scaled = value * 1000.0;
if (scaled > (double) UINT32_MAX) scaled = (double) UINT32_MAX;
return (uint32_t)(scaled + 0.5);
}
/**
* @brief Read a Linux Pressure Stall Information (PSI) file.
*
* Parses @c /proc/pressure/{cpu,io,memory} and populates the six output
* pointers with avg10/avg60/avg300 milli-values for both the @c some and
* @c full stall categories. Any output pointer may be NULL to skip that field.
* Returns a bitmask: bit 0 set if @c some line was found, bit 1 if @c full.
*
* @param path Path to PSI file (e.g. @c "/proc/pressure/cpu")
* @param some10 avg10 for the @c some stall category (milli-units), or NULL
* @param some60 avg60 for the @c some stall category, or NULL
* @param some300 avg300 for the @c some stall category, or NULL
* @param full10 avg10 for the @c full stall category, or NULL
* @param full60 avg60 for the @c full stall category, or NULL
* @param full300 avg300 for the @c full stall category, or NULL
* @return Bitmask of parsed sections (0 = none, 1 = some, 2 = full, 3 = both)
*/
static int read_psi_file(const char *path,
uint32_t *some10, uint32_t *some60, uint32_t *some300,
uint32_t *full10, uint32_t *full60, uint32_t *full300) {
FILE *f = fopen(path, "r");
if (!f) {
return 0;
}
char line[256];
int mask = 0;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "some", 4) == 0) {
if (some10) *some10 = parse_avg_milli(line, "avg10=");
if (some60) *some60 = parse_avg_milli(line, "avg60=");
if (some300) *some300 = parse_avg_milli(line, "avg300=");
mask |= 1;
} else if (strncmp(line, "full", 4) == 0) {
if (full10) *full10 = parse_avg_milli(line, "avg10=");
if (full60) *full60 = parse_avg_milli(line, "avg60=");
if (full300) *full300 = parse_avg_milli(line, "avg300=");
mask |= 2;
}
}
fclose(f);
return mask;
}
/**
* @brief Read aggregate CPU jiffy totals from @c /proc/stat.
*
* Parses the first @c cpu line from @c /proc/stat and sums user, nice, system,
* idle, iowait, irq, softirq, and steal columns into @c *total. The idle
* estimate is @c idle_j + @c iowait. Either output pointer may be NULL.
*
* @param total Output: total jiffy count across all categories
* @param idle Output: idle + iowait jiffies (effective idle)
* @return 0 on success, -1 on file open or parse failure
*/
static int read_proc_stat_totals(uint64_t *total, uint64_t *idle) {
FILE *f = fopen("/proc/stat", "r");
if (!f) {
return -1;
}
char line[256];
if (!fgets(line, sizeof(line), f)) {
fclose(f);
return -1;
}
fclose(f);
unsigned long long user = 0, nice = 0, system = 0, idle_j = 0;
unsigned long long iowait = 0, irq = 0, softirq = 0, steal = 0;
int scanned = sscanf(line, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
&user, &nice, &system, &idle_j, &iowait, &irq, &softirq, &steal);
if (scanned < 4) {
return -1;
}
unsigned long long total_j = user + nice + system + idle_j + iowait + irq + softirq + steal;
if (total) *total = total_j;
if (idle) *idle = idle_j + iowait;
return 0;
}
/**
* @brief Resolve the cgroup v2 filesystem path for the current process.
*
* Reads @c /proc/self/cgroup, finds the cgroup2 hierarchy entry (the line
* with @c "::" separator), and constructs the full sysfs path under
* @c /sys/fs/cgroup. The root cgroup ("/") maps to @c /sys/fs/cgroup itself.
*
* @param out Output buffer for the resolved path
* @param out_sz Size of @c out in bytes
* @return 0 on success, -1 if the path could not be resolved
*/
static int resolve_cgroup_path(char *out, size_t out_sz) {
FILE *f = fopen("/proc/self/cgroup", "r");
if (!f) {
return -1;
}
char line[512];
int rc = -1;
while (fgets(line, sizeof(line), f)) {
char *sep = strstr(line, "::");
if (!sep) continue;
char *path = sep + 2;
char *newline = strchr(path, '\n');
if (newline) *newline = '\0';
if (*path == '\0' || strcmp(path, "/") == 0) {
if (snprintf(out, out_sz, "%s", "/sys/fs/cgroup") < (int) out_sz) {
rc = 0;
break;
}
} else {
if (snprintf(out, out_sz, "/sys/fs/cgroup%s", path) < (int) out_sz) {
rc = 0;
break;
}
}
}
fclose(f);
return rc;
}
/**
* @brief Read the cumulative CPU usage in microseconds from a cgroup's @c cpu.stat.
*
* Opens @c <cgpath>/cpu.stat and extracts the @c usage_usec field.
* Returns 0 if the file cannot be opened or the field is absent.
*
* @param cgpath Absolute path to the cgroup directory
* @return Cumulative CPU usage in microseconds, or 0 on failure
*/
static uint64_t read_cgroup_cpu_usage_us(const char *cgpath) {
char path[PATH_MAX];
if (snprintf(path, sizeof(path), "%s/cpu.stat", cgpath) >= (int) sizeof(path)) {
return 0;
}
FILE *f = fopen(path, "r");
if (!f) {
return 0;
}
char line[256];
uint64_t value = 0;
while (fgets(line, sizeof(line), f)) {
if (sscanf(line, "usage_usec %" SCNu64, &value) == 1) {
break;
}
}
fclose(f);
return value;
}
/**
* @brief Read current memory usage in bytes from a cgroup's @c memory.current.
*
* Opens @c <cgpath>/memory.current and reads the single integer value.
* Returns 0 if the file cannot be opened or parsed.
*
* @param cgpath Absolute path to the cgroup directory
* @return Current memory usage in bytes, or 0 on failure
*/
static uint64_t read_cgroup_memory_current(const char *cgpath) {
char path[PATH_MAX];
if (snprintf(path, sizeof(path), "%s/memory.current", cgpath) >= (int) sizeof(path)) {
return 0;
}
FILE *f = fopen(path, "r");
if (!f) {
return 0;
}
unsigned long long value = 0;
if (fscanf(f, "%llu", &value) != 1) {
value = 0;
}
fclose(f);
return value;
}
/* end of the section historically wrapped in `#if !defined(__l4__)` */
/**
* @brief Allocate and initialise the HOLA analytics heap.
*
* Allocates a contiguous block of at least @c requested_bytes (or
* @c PHYSICS_ANALYTICS_DEFAULT_HEAP_BYTES if 0), enforcing a minimum that
* accommodates the header plus 512 KB ring buffer. Partitions the block into
* header (cache-line aligned), ring (60%), summary (30%), and scratch regions,
* then populates the @c physics_analytics_header_t with magic, version, and
* offsets. Safe to call repeatedly — returns 0 immediately if already ready.
*
* @param requested_bytes Desired heap size in bytes (0 = use default)
* @return 0 on success, -1 if allocation failed
*/
static int analytics_heap_allocate(size_t requested_bytes) {
if (g_runtime.heap_ready) {
return 0;
}
size_t bytes = requested_bytes;
if (bytes == 0) {
bytes = PHYSICS_ANALYTICS_DEFAULT_HEAP_BYTES;
}
/* Enforce minimum viable size: header + ring + summary */
const size_t min_bytes = align_up(sizeof(physics_analytics_header_t), 64) + (512u * 1024u);
if (bytes < min_bytes) {
bytes = min_bytes;
}
uint8_t *base = NULL;
/* HISTORICAL: this used to be an #if !defined(__l4__)/#else branch --
* both did an identical malloc(), with the L4Re side noting "simple
* malloc is fine for now; future work can switch to dataspace". L4Re
* support has been removed as an active target. */
base = (uint8_t *) malloc(bytes);
if (!base) {
return -1;
}
memset(base, 0, bytes);
physics_analytics_heap_t heap = {0};
heap.base = base;
heap.bytes = bytes;
size_t header_bytes = align_up(sizeof(physics_analytics_header_t), 64);
heap.header = (physics_analytics_header_t *) base;
/* Split remaining space into ring (60%), summary (30%), scratch (remainder) */
size_t remaining = bytes - header_bytes;
size_t ring_bytes = align_up((remaining * 6u) / 10u, 64);
if (ring_bytes > remaining) ring_bytes = remaining;
size_t summary_bytes = remaining > ring_bytes ? align_up((remaining - ring_bytes) * 3u / 4u, 64) : 0;
if (summary_bytes > remaining - ring_bytes) summary_bytes = remaining - ring_bytes;
size_t scratch_bytes = remaining - ring_bytes - summary_bytes;
heap.ring = base + header_bytes;
heap.ring_bytes = ring_bytes;
heap.summary = heap.ring + ring_bytes;
heap.summary_bytes = summary_bytes;
heap.scratch = heap.summary + summary_bytes;
heap.scratch_bytes = scratch_bytes;
/* Populate header */
physics_analytics_header_t *hdr = heap.header;
hdr->magic = HOLA_SHARED_MAGIC;
hdr->version_major = HOLA_PROTOCOL_MAJOR;
hdr->version_minor = HOLA_PROTOCOL_MINOR;
hdr->heap_bytes = (uint32_t) bytes;
hdr->ring_offset = (uint32_t) header_bytes;
hdr->ring_bytes = (uint32_t) ring_bytes;
hdr->summary_offset = (uint32_t)(heap.summary - heap.base);
hdr->summary_bytes = (uint32_t) summary_bytes;
hdr->scratch_offset = (uint32_t)(heap.scratch - heap.base);
hdr->scratch_bytes = (uint32_t) scratch_bytes;
hdr->produce_seq = 0;
hdr->consume_seq = 0;
hdr->write_offset = 0;
hdr->read_offset = 0;
hdr->dropped_events = 0;
hdr->flags = 0;
g_runtime.heap = heap;
g_runtime.heap_ready = 1;
g_runtime.backend_seq = 0;
return 0;
}
/**
* @brief Free the analytics heap and reset all global heap state.
*
* Safe to call when the heap is not ready. After this call,
* @c g_runtime.heap_ready is 0 and the heap pointer is NULL.
*/
static void analytics_heap_release(void) {
if (!g_runtime.heap_ready) {
return;
}
free(g_runtime.heap.base);
g_runtime.heap = (physics_analytics_heap_t) {
0
};
g_runtime.heap_ready = 0;
g_runtime.backend_seq = 0;
}
/**
* @brief Compute free bytes available in the analytics ring buffer.
*
* Assumes single-producer, single-consumer access with no concurrent writers.
* Clamps out-of-range @c write_offset and @c read_offset before computing.
*
* @param hdr Pointer to the analytics heap header
* @return Number of free bytes available for a new event write
*/
static size_t analytics_ring_free(const physics_analytics_header_t *hdr) {
uint32_t write = hdr->write_offset;
uint32_t read = hdr->read_offset;
uint32_t size = hdr->ring_bytes;
if (write >= size) write = 0;
if (read >= size) read = 0;
if (write >= read) {
return (size_t) (size - write + read - 1u);
}
return (size_t) (read - write - 1u);
}
/* ------------------------------------------------------------------------- */
/* Public API */
/* ------------------------------------------------------------------------- */
/**
* @brief Initialise the physics runtime and analytics heap.
*
* Must be called before any other physics_runtime_* function.
* Idempotent — safe to call multiple times; subsequent calls return 0
* immediately without re-allocating.
*
* @param analytics_heap_bytes Desired analytics heap size (0 = default)
* @return 0 on success, -1 on allocation failure
*/
int physics_runtime_init(size_t analytics_heap_bytes) {
return analytics_heap_allocate(analytics_heap_bytes);
}
/**
* @brief Shut down the physics runtime and release the analytics heap.
*
* Safe to call when the runtime was never initialised. After this call
* the analytics heap is gone and no events can be published until
* @c physics_runtime_init() is called again.
*/
void physics_runtime_shutdown(void) {
analytics_heap_release();
}
/**
* @brief Return a pointer to the live analytics heap descriptor.
*
* @return Pointer to the heap descriptor, or NULL if not yet initialised
*/
const physics_analytics_heap_t *physics_analytics_heap_info(void) {
return g_runtime.heap_ready ? &g_runtime.heap : NULL;
}
/**
* @brief Publish an analytics event into the ring buffer.
*
* Writes a @c physics_analytics_event_header_t followed by the payload
* (8-byte padded) into the ring at the current write offset. If insufficient
* space is available, increments @c dropped_events and returns -1. The event
* is timestamped with @c sf_monotonic_ns() at write time.
*
* @param channel Event channel identifier (e.g. @c PHYSICS_ANALYTICS_CHANNEL_HOST_SNAPSHOT)
* @param payload Event payload bytes (must not be NULL)
* @param payload_bytes Length of payload in bytes
* @return 0 on success, -1 if the heap is not ready, payload is NULL, or ring is full
*/
int physics_analytics_publish_event(uint32_t channel, const void *payload, uint16_t payload_bytes) {
if (!g_runtime.heap_ready || !payload) {
return -1;
}
physics_analytics_header_t *hdr = g_runtime.heap.header;
uint8_t *ring = g_runtime.heap.ring;
uint32_t ring_bytes = hdr->ring_bytes;
if (ring_bytes == 0) {
return -1;
}
size_t padded_payload = align_up(payload_bytes, 8);
size_t total_bytes = sizeof(physics_analytics_event_header_t) + padded_payload;
if (total_bytes >= ring_bytes) {
/* Payload cannot fit even into an empty ring */
hdr->dropped_events++;
return -1;
}
size_t free_bytes = analytics_ring_free(hdr);
if (total_bytes > free_bytes) {
hdr->dropped_events++;
return -1;
}
uint32_t write = hdr->write_offset;
if (write >= ring_bytes) {
write = 0;
}
if (write + total_bytes > ring_bytes) {
size_t tail = ring_bytes - write;
memset(ring + write, 0, tail);
write = 0;
}
physics_analytics_event_header_t *evt = (physics_analytics_event_header_t *) (ring + write);
evt->channel = channel;
evt->payload_bytes = payload_bytes;
evt->reserved = 0;
evt->timestamp_ns = sf_monotonic_ns();
memcpy((uint8_t *) (evt + 1), payload, payload_bytes);
if (padded_payload > payload_bytes) {
memset(((uint8_t *) (evt + 1)) + payload_bytes, 0, padded_payload - payload_bytes);
}
write += (uint32_t) total_bytes;
if (write >= ring_bytes) {
write = 0;
}
hdr->write_offset = write;
hdr->produce_seq++;
return 0;
}
/* ------------------------------------------------------------------------- */
/* Host snapshot shim */
/* ------------------------------------------------------------------------- */
/**
* @brief Collect a host system snapshot using POSIX / Linux interfaces.
*
* Populates @c out with monotonic and realtime timestamps, CPU count,
* scheduler policy and priority, load average (scaled × 1000), runnable
* thread estimate, PSI pressure readings, @c /proc/stat CPU jiffy totals,
* and cgroup v2 CPU usage and memory current values. Sets flags in
* @c out->flags to indicate which fields were successfully populated.
*
* @param out Output snapshot struct; zeroed and populated by this function
* @return 0 on success
*/
static int snapshot_posix(physics_host_snapshot_t *out) {
memset(out, 0, sizeof(*out));
out->backend = PHYSICS_HOST_BACKEND_POSIX;
out->monotonic_time_ns = sf_monotonic_ns();
out->realtime_ns = sf_realtime_ns();
long cpu_count = sysconf(_SC_NPROCESSORS_ONLN);
out->cpu_count = (cpu_count > 0) ? (uint32_t) cpu_count : 1u;
/* Scheduler information */
int policy = sched_getscheduler(0);
if (policy >= 0) {
out->scheduler_policy = (uint32_t) policy;
struct sched_param param = {0};
if (sched_getparam(0, &param) == 0) {
out->scheduler_priority = (uint32_t) param.sched_priority;
}
#if defined(_POSIX_PRIORITY_SCHEDULING)
if (policy == SCHED_RR) {
struct timespec ts = {0};
if (sched_rr_get_interval(0, &ts) == 0) {
out->scheduler_quantum_ns = (uint32_t)((ts.tv_sec * 1000000000LL) + ts.tv_nsec);
}
}
#endif
}
/* Load estimate (scaled by 1000) */
#if defined(_GNU_SOURCE)
double loads[3] = {0.0, 0.0, 0.0};
if (getloadavg(loads, 3) > 0) {
out->load_avg_milli = (uint32_t)(loads[0] * 1000.0);
}
#endif
/* Runnable thread estimate: best effort via load average */
if (out->load_avg_milli > 0) {
out->runnable_threads = (out->load_avg_milli + 999u) / 1000u;
}
int psi_mask = 0;
psi_mask |= read_psi_file("/proc/pressure/cpu",
&out->psi_cpu_some_avg10_milli,
&out->psi_cpu_some_avg60_milli,
&out->psi_cpu_some_avg300_milli,
&out->psi_cpu_full_avg10_milli,
&out->psi_cpu_full_avg60_milli,
&out->psi_cpu_full_avg300_milli);
psi_mask |= read_psi_file("/proc/pressure/io",
&out->psi_io_some_avg10_milli,
&out->psi_io_some_avg60_milli,
&out->psi_io_some_avg300_milli,
&out->psi_io_full_avg10_milli,
&out->psi_io_full_avg60_milli,
&out->psi_io_full_avg300_milli);
psi_mask |= read_psi_file("/proc/pressure/memory",
&out->psi_mem_some_avg10_milli,
&out->psi_mem_some_avg60_milli,
&out->psi_mem_some_avg300_milli,
NULL, NULL, NULL);
if (psi_mask) {
out->flags |= PHYSICS_HOST_FLAG_PSI;
}
uint64_t total_jiffies = 0;
uint64_t idle_jiffies = 0;
if (read_proc_stat_totals(&total_jiffies, &idle_jiffies) == 0) {
out->cpu_total_jiffies = total_jiffies;
out->cpu_idle_jiffies = idle_jiffies;
out->flags |= PHYSICS_HOST_FLAG_CPU_STAT;
}
char cgpath[PATH_MAX];
if (resolve_cgroup_path(cgpath, sizeof(cgpath)) == 0) {
uint64_t usage = read_cgroup_cpu_usage_us(cgpath);
uint64_t mem_cur = read_cgroup_memory_current(cgpath);
out->cgroup_cpu_usage_us = usage;
out->cgroup_memory_current_bytes = mem_cur;
if (usage != 0 || mem_cur != 0) {
out->flags |= PHYSICS_HOST_FLAG_CGROUP;
}
}
out->backend_seq = ++g_runtime.backend_seq;
return 0;
}
/* HISTORICAL: an L4Re backend used to sit here, selected ahead of
* snapshot_posix() by physics_host_snapshot() below whenever __l4__ was
* defined. L4Re support has been removed as an active target.
*
* static int snapshot_l4re(physics_host_snapshot_t *out) {
* memset(out, 0, sizeof(*out));
* out->backend = PHYSICS_HOST_BACKEND_L4RE;
* out->monotonic_time_ns = sf_monotonic_ns();
* out->realtime_ns = sf_realtime_ns();
*
* l4_kernel_info_t *kip = l4re_kip();
* (void) kip;
* out->cpu_count = 1u; // TODO: query processor count from KIP once available
*
* out->backend_seq = ++g_runtime.backend_seq;
* return 0;
* }
*/
/**
* @brief Capture a host environment snapshot and publish it to the analytics ring.
*
* Uses the POSIX backend. On success also publishes the snapshot as a
* @c PHYSICS_ANALYTICS_CHANNEL_HOST_SNAPSHOT event to the analytics ring
* buffer (if the heap is initialised).
*
* @param out Output snapshot struct to populate (must not be NULL)
* @return 0 on success, -1 if @c out is NULL or the backend fails
*/
int physics_host_snapshot(physics_host_snapshot_t *out) {
if (!out) {
return -1;
}
int rc;
/* HISTORICAL: an #ifdef __l4__ block here tried snapshot_l4re() first
* and `goto publish` past snapshot_posix() on success. L4Re support
* has been removed as an active target. */
rc = snapshot_posix(out);
if (rc != 0) {
return rc;
}
if (g_runtime.heap_ready) {
physics_analytics_publish_event(PHYSICS_ANALYTICS_CHANNEL_HOST_SNAPSHOT,
out,
(uint16_t) sizeof(*out));
}
return 0;
}
#endif /* STARFORTH_MINIMAL */
/* ------------------------------------------------------------------------- */
/* End */
/* ------------------------------------------------------------------------- */
+20
View File
@@ -0,0 +1,20 @@
# src/platform/
Platform abstraction layer for the hosted VM build.
- `alloc_host.c` — hosted (`malloc`-backed) allocator implementation of
`include/platform_alloc.h`.
- `alloc_kernel.c` — a kernel-side allocator implementation of the same
interface. Note: `src/starkernel/vm/alloc_kernel.c` is the copy actually
referenced by `Makefile.starkernel`'s kernel build; this one under
`src/platform/` is not wired into any build target — check before relying
on it.
- `platform_init.c` — platform-specific initialization dispatch.
- `threading.c` — threading primitives backing the heartbeat background
thread (`HEARTBEAT_THREAD_ENABLED=1`).
Subdirectories:
- `linux/time.c` — POSIX (`clock_gettime`) timing backend.
- `l4re/time.c` — HISTORICAL: L4Re/Fiasco.OC timing backend. L4Re was a
supported platform target through mid-2026; it has been removed as an
active target and this file is no longer wired into any build.
+164
View File
@@ -0,0 +1,164 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* platform/alloc_host.c - Host (Linux/POSIX) memory allocator
*
* Simple wrapper around standard malloc/free with statistics tracking.
*/
#include "../../include/platform_alloc.h"
#include <stdlib.h>
#include <string.h>
/* Statistics tracking */
static sf_alloc_stats_t g_stats = {0};
/**
* @brief Initialise the host allocator statistics.
*
* Resets the @c g_stats struct to zero. Safe to call multiple times;
* subsequent calls simply reset the counters. No heap memory is allocated
* by this function — the underlying allocator is the system @c malloc.
*
* @return 0 always (cannot fail on the hosted path)
*/
int sf_alloc_init(void)
{
memset(&g_stats, 0, sizeof(g_stats));
return 0;
}
/**
* @brief Allocate @c size bytes from the system heap.
*
* Delegates to @c malloc(). On success, increments @c g_stats.used_bytes
* and @c g_stats.alloc_count, and updates @c g_stats.peak_bytes if the
* new used total exceeds the recorded peak.
*
* @param size Number of bytes to allocate; 0 returns NULL
* @return Pointer to allocated memory, or NULL on allocation failure or zero size
*/
void* sf_malloc(size_t size)
{
if (size == 0) return NULL;
void* ptr = malloc(size);
if (ptr)
{
g_stats.used_bytes += size;
g_stats.alloc_count++;
if (g_stats.used_bytes > g_stats.peak_bytes)
{
g_stats.peak_bytes = g_stats.used_bytes;
}
}
return ptr;
}
/**
* @brief Allocate a zero-initialised array of @c count elements of @c size bytes each.
*
* Delegates to @c calloc(). Updates statistics the same way as @c sf_malloc():
* adds @c count*size to @c used_bytes, increments @c alloc_count, and updates
* @c peak_bytes.
*
* @param count Number of elements; 0 returns NULL
* @param size Size of each element in bytes; 0 returns NULL
* @return Pointer to zero-filled memory, or NULL on allocation failure or zero arguments
*/
void* sf_calloc(size_t count, size_t size)
{
if (count == 0 || size == 0) return NULL;
void* ptr = calloc(count, size);
if (ptr)
{
size_t total = count * size;
g_stats.used_bytes += total;
g_stats.alloc_count++;
if (g_stats.used_bytes > g_stats.peak_bytes)
{
g_stats.peak_bytes = g_stats.used_bytes;
}
}
return ptr;
}
/**
* @brief Resize a previously allocated block to @c new_size bytes.
*
* Delegates to @c realloc(). On success, increments @c g_stats.alloc_count.
* The exact byte delta is not tracked because the original allocation size is
* not stored alongside the pointer — @c used_bytes is therefore approximate
* after any @c sf_realloc() call. For accurate accounting, wrap allocations
* with size-tracking headers.
*
* @param ptr Pointer to an existing allocation, or NULL (behaves like @c sf_malloc)
* @param new_size Desired new size in bytes; 0 may free the block (platform-defined)
* @return Pointer to resized memory, or NULL on failure (original block unchanged)
*/
void* sf_realloc(void* ptr, size_t new_size)
{
void* new_ptr = realloc(ptr, new_size);
if (new_ptr)
{
/* Note: Can't track exact delta without size tracking */
g_stats.alloc_count++;
}
return new_ptr;
}
/**
* @brief Release a previously allocated block back to the system heap.
*
* Delegates to @c free(). Increments @c g_stats.free_count on non-NULL @c ptr.
* The freed byte count is not subtracted from @c used_bytes because the
* original size is not tracked — see @c sf_realloc() note.
*
* @param ptr Pointer to free; NULL is a safe no-op
*/
void sf_free(void* ptr)
{
if (!ptr) return;
/* Note: We can't track exact bytes freed without size tracking.
* For accurate stats, would need to wrap allocations with headers.
* For now, just count frees. */
g_stats.free_count++;
free(ptr);
}
/**
* @brief Copy the current allocator statistics into @c stats.
*
* Fills @c *stats with a snapshot of @c g_stats. Note that @c used_bytes
* overstates the live total when blocks have been freed or reallocated,
* because freed sizes are not tracked. @c free_count is accurate.
*
* @param stats Output buffer to populate; no-op if NULL
*/
void sf_alloc_get_stats(sf_alloc_stats_t* stats)
{
if (!stats) return;
*stats = g_stats;
}
+225
View File
@@ -0,0 +1,225 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* platform/alloc_kernel.c - Kernel (bare-metal) memory allocator
*
* Static arena with bump allocation. Free is a no-op.
* This is appropriate for kernel use where:
* - VM is long-lived (no restart)
* - Allocations happen at init time
* - Runtime allocations are rare
*
* Arena size: 4MB by default (configurable via SF_ARENA_SIZE)
*/
#include "../../include/platform_alloc.h"
#include <stdint.h>
#ifndef SF_ARENA_SIZE
#define SF_ARENA_SIZE (4 * 1024 * 1024) /* 4MB default */
#endif
/* Alignment for all allocations (8 bytes for 64-bit safety) */
#define SF_ALIGN 8
#define SF_ALIGN_UP(x) (((x) + (SF_ALIGN - 1)) & ~(SF_ALIGN - 1))
/* Static arena */
static uint8_t g_arena[SF_ARENA_SIZE] __attribute__((aligned(SF_ALIGN)));
static size_t g_arena_offset = 0;
static int g_initialized = 0;
/* Statistics */
static sf_alloc_stats_t g_stats = {0};
/**
* @brief Initialise the kernel bump allocator.
*
* Resets @c g_arena_offset to zero and clears all @c g_stats fields.
* Sets @c g_initialized so that @c sf_malloc() skips the lazy-init guard.
* Safe to call at any point; all prior allocations become invalid after a
* reset, so this must only be called before any VM allocation takes place.
*
* @return 0 always (cannot fail)
*/
int sf_alloc_init(void)
{
g_arena_offset = 0;
g_initialized = 1;
g_stats.total_bytes = SF_ARENA_SIZE;
g_stats.used_bytes = 0;
g_stats.peak_bytes = 0;
g_stats.alloc_count = 0;
g_stats.free_count = 0;
return 0;
}
/*
* @brief Allocate @c size bytes from the static kernel arena.
*
* Advances @c g_arena_offset by @c SF_ALIGN_UP(size). Returns a pointer
* directly into @c g_arena; the caller must not @c free() the result.
* Returns NULL (cast 0) when the arena is exhausted or @c size is zero.
* Lazy-calls @c sf_alloc_init() if the arena has not yet been initialised.
*
* @param size Number of bytes to allocate; 0 returns NULL
* @return Pointer into the static arena, or NULL on exhaustion or zero size
*/
void* sf_malloc(size_t size)
{
if (!g_initialized) sf_alloc_init();
if (size == 0) return (void*)0;
size_t aligned_size = SF_ALIGN_UP(size);
size_t new_offset = g_arena_offset + aligned_size;
if (new_offset > SF_ARENA_SIZE)
{
/* Out of memory */
return (void*)0;
}
void* ptr = &g_arena[g_arena_offset];
g_arena_offset = new_offset;
g_stats.used_bytes = g_arena_offset;
g_stats.alloc_count++;
if (g_stats.used_bytes > g_stats.peak_bytes)
{
g_stats.peak_bytes = g_stats.used_bytes;
}
return ptr;
}
/*
* @brief Allocate a zero-initialised block of @c count * @c size bytes from the arena.
*
* Delegates to @c sf_malloc() then manually zeroes the result with a byte loop
* (no @c memset dependency — avoids linking the C runtime in freestanding builds).
* Guards against @c count*size overflow by verifying @c total/size == count.
*
* @param count Number of elements; 0 returns NULL
* @param size Size of each element in bytes; 0 returns NULL
* @return Pointer to zero-filled arena memory, or NULL on overflow/exhaustion/zero args
*/
void* sf_calloc(size_t count, size_t size)
{
if (count == 0 || size == 0) return (void*)0;
size_t total = count * size;
/* Check for overflow */
if (size != 0 && total / size != count)
{
return (void*)0;
}
void* ptr = sf_malloc(total);
if (ptr)
{
/* Zero-initialize */
uint8_t* p = (uint8_t*)ptr;
for (size_t i = 0; i < total; i++)
{
p[i] = 0;
}
}
return ptr;
}
/*
* @brief Resize an allocation — bump-allocator approximation.
*
* Because the arena does not store block sizes, the old block cannot be
* reclaimed. This function allocates a fresh block of @c new_size bytes and
* returns it; the old block is orphaned in the arena. The caller is responsible
* for copying any content from @c ptr to the returned pointer before discarding
* @c ptr. If @c ptr is NULL, behaves identically to @c sf_malloc(new_size).
*
* @param ptr Existing allocation (orphaned after this call); NULL is safe
* @param new_size Desired size in bytes; 0 returns NULL
* @return Pointer to a new arena block of at least @c new_size bytes, or NULL
*/
void* sf_realloc(void* ptr, size_t new_size)
{
/*
* Bump allocator limitation: we don't track allocation sizes.
* For kernel use, realloc is rarely needed.
*
* Strategy: allocate new block, caller must copy if needed.
* This wastes memory but works for rare realloc cases.
*
* If ptr is NULL, this is equivalent to malloc.
*/
if (!ptr) return sf_malloc(new_size);
if (new_size == 0) return (void*)0;
/* Allocate new block - old block is orphaned (bump allocator limitation) */
return sf_malloc(new_size);
}
/*
* @brief Release a block — no-op for the bump allocator.
*
* The kernel arena has no reclaim mechanism; memory is held until the
* next @c sf_alloc_init() call (i.e. the next power cycle). Increments
* @c g_stats.free_count so the caller can observe that @c sf_free() was
* invoked, but no bytes are returned to the arena.
*
* @param ptr Block to "free"; NULL is a safe no-op
*/
void sf_free(void* ptr)
{
/* Bump allocator: free is a no-op.
*
* This is acceptable because:
* 1. VM allocations happen at init time
* 2. VM runs until power-off
* 3. No fragmentation issues in practice
*
* If needed, could implement a simple free list here.
*/
if (ptr)
{
g_stats.free_count++;
}
(void)ptr;
}
/*
* @brief Copy the current arena statistics into @c stats.
*
* Fills @c *stats with a snapshot of @c g_stats. @c total_bytes is fixed at
* @c SF_ARENA_SIZE; @c used_bytes equals the current arena offset (exact,
* since no reclaim occurs); @c free_count tracks @c sf_free() call count
* even though no memory is actually released.
*
* @param stats Output buffer to populate; no-op if NULL
*/
void sf_alloc_get_stats(sf_alloc_stats_t* stats)
{
if (!stats) return;
*stats = g_stats;
}
+6
View File
@@ -0,0 +1,6 @@
# src/platform/l4re/
- `time.c` — HISTORICAL: L4Re/Fiasco.OC timing backend implementing
`include/platform_time.h`. L4Re was a supported platform target through
mid-2026; it has been removed as an active target, and this file is no
longer wired into any build (hosted or kernel).
+300
View File
@@ -0,0 +1,300 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* StarForth Platform Time - L4Re/StarshipOS Backend
*
* HISTORICAL: L4Re/Fiasco.OC was a supported platform target through
* mid-2026 (`make L4RE=1`). Removed as an active target -- no Makefile
* defines __l4__ anymore, so the entire body of this file is unreachable
* dead code. Retained for reference; see Makefile's "Platform support"
* section for the commented-out restoration path if L4Re support is ever
* revived.
*
* Implements platform time abstraction using L4Re RTC server and KIP clock.
*
* License: See LICENSE file
*/
#include "../../../include/platform_time.h"
#ifdef __l4__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* L4Re headers */
#include <l4/sys/kip.h>
#include <l4/re/env.h>
#include <l4/rtc/rtc>
/* L4Re RTC capability (obtained at init) */
static L4::Cap<L4rtc::Rtc> rtc_cap = L4::Cap<L4rtc::Rtc>::Invalid;
static l4_uint64_t rtc_offset = 0; /* nanoseconds from 1970 to boot */
static int rtc_available = 0;
/* L4Re implementation */
/**
* @brief Return the current monotonic time in nanoseconds (L4Re build).
*
* Reads the KIP (Kernel Info Page) clock via @c l4_kip_clock_ns(). The KIP
* clock is a monotonic counter driven by the L4Re microkernel and is the
* standard high-resolution time source on L4Re. Returns 0 if the KIP pointer
* is unavailable (should never occur in a healthy L4Re environment).
*
* @return Nanoseconds since the L4Re epoch (system boot), or 0 on KIP error
*/
static sf_time_ns_t l4re_get_monotonic_ns(void) {
/* Use L4Re KIP clock: monotonic time since boot */
l4_kernel_info_t *kip = l4re_kip();
if (!kip) {
return 0; /* Fallback if KIP unavailable */
}
return l4_kip_clock_ns(kip);
}
/**
* @brief Return the current wall-clock time in nanoseconds since the Unix epoch (L4Re build).
*
* Combines the RTC offset (nanoseconds from epoch to system boot, obtained
* from the L4Re RTC server during @c sf_time_init_l4re()) with the current
* KIP clock value to produce an absolute timestamp. Falls back to monotonic
* time alone when @c rtc_available is 0, and to @c rtc_offset alone when
* the KIP pointer is unavailable.
*
* @return Nanoseconds since 1970-01-01 00:00:00 UTC, or approximate value on fallback
*/
static sf_time_ns_t l4re_get_realtime_ns(void) {
/* Use RTC server offset + KIP clock for wall-clock time */
if (!rtc_available) {
/* No RTC: return monotonic time (better than nothing) */
return l4re_get_monotonic_ns();
}
/* Real-time = RTC offset (ns since epoch to boot) + KIP clock (ns since boot) */
l4_kernel_info_t *kip = l4re_kip();
if (!kip) {
return rtc_offset; /* Fallback to RTC offset only */
}
return rtc_offset + l4_kip_clock_ns(kip);
}
/**
* @brief Set the system real-time clock via the L4Re RTC server (L4Re build).
*
* Computes the new RTC timer offset as @c ns_since_epoch minus the current
* KIP clock value, then calls @c rtc_cap->set_timer_offset() over IPC.
* On success, updates the cached @c rtc_offset. Returns -1 if the RTC
* capability is invalid, the KIP pointer is unavailable, or the IPC call
* fails.
*
* @param ns_since_epoch Desired wall-clock time in nanoseconds since Unix epoch
* @return 0 on success, -1 on any failure (no RTC capability, KIP error, IPC error)
*/
static int l4re_set_realtime_ns(sf_time_ns_t ns_since_epoch) {
if (!rtc_available || !rtc_cap.is_valid()) {
return -1; /* No RTC capability */
}
/* Calculate new offset: desired epoch time - current KIP clock */
l4_kernel_info_t *kip = l4re_kip();
if (!kip) {
return -1;
}
l4_uint64_t kip_ns = l4_kip_clock_ns(kip);
l4_uint64_t new_offset = ns_since_epoch - kip_ns;
/* Set RTC offset via IPC */
long ret = rtc_cap->set_timer_offset(new_offset);
if (ret < 0) {
return -1;
}
/* Update cached offset */
rtc_offset = new_offset;
return 0;
}
/**
* @brief Format a nanosecond-precision wall-clock time as a human-readable string (L4Re build).
*
* Converts @c ns_since_epoch to a @c time_t, calls @c localtime() and
* @c strftime() using the L4Re libc. Unlike the Linux backend, this variant
* uses a full date+time format (@c "%Y-%m-%d %H:%M:%S" or @c "%Y-%m-%d %I:%M:%S %p")
* rather than time-only. On @c localtime() failure, writes the raw second count as
* a decimal fallback and returns -1.
*
* @param ns_since_epoch Wall-clock time in nanoseconds since Unix epoch
* @param buf Output buffer (at least @c SF_TIME_STAMP_SIZE bytes); must not be NULL
* @param format_24h Non-zero for 24-hour format, zero for 12-hour with AM/PM
* @return 0 on success, -1 if @c buf is NULL or time formatting fails
*/
static int l4re_format_timestamp(sf_time_ns_t ns_since_epoch, char *buf, int format_24h) {
if (!buf) {
return -1;
}
/* Convert nanoseconds to seconds for libc time functions */
time_t seconds = (time_t)(ns_since_epoch / 1000000000ULL);
/* Use L4Re libc localtime + strftime */
struct tm *tm_info = localtime(&seconds);
if (!tm_info) {
/* Fallback: show raw seconds */
snprintf(buf, SF_TIME_STAMP_SIZE, "%llu", (unsigned long long) seconds);
return -1;
}
/* Format timestamp based on 24h preference */
const char *format = format_24h ? "%Y-%m-%d %H:%M:%S" : "%Y-%m-%d %I:%M:%S %p";
size_t ret = strftime(buf, SF_TIME_STAMP_SIZE, format, tm_info);
return (ret > 0) ? 0 : -1;
}
/**
* @brief Report whether an RTC is available (L4Re build).
*
* Returns the @c rtc_available flag set during @c sf_time_init_l4re().
* A value of 0 means the RTC IPC capability was absent or its offset query
* failed; wall-clock functions will return monotonic time as a fallback.
*
* @return 1 if the L4Re RTC server is reachable and its offset was obtained, 0 otherwise
*/
static int l4re_has_rtc(void) {
return rtc_available;
}
/* L4Re backend vtable */
const sf_time_backend_t sf_time_backend_l4re = {
.get_monotonic_ns = l4re_get_monotonic_ns,
.get_realtime_ns = l4re_get_realtime_ns,
.set_realtime_ns = l4re_set_realtime_ns,
.format_timestamp = l4re_format_timestamp,
.has_rtc = l4re_has_rtc,
};
/**
* @brief Initialise the L4Re time backend — acquire RTC capability and fetch epoch offset.
*
* Called once from @c sf_time_init() on L4Re builds. Attempts to obtain the
* @c "rtc" capability from the L4Re environment, then queries the RTC server
* for its timer offset (nanoseconds from the Unix epoch to the system boot
* time). On success sets @c rtc_available = 1 and caches @c rtc_offset.
* On any failure (missing capability, IPC error) sets @c rtc_available = 0
* and @c rtc_offset = 0, leaving the backend in monotonic-only mode.
* Diagnostic messages are printed to @c stderr throughout.
*
* @note Defined only when @c __l4__ is set; compiles to a no-op stub otherwise.
*/
void sf_time_init_l4re(void) {
fprintf(stderr, "[L4Re Time] Initializing L4Re time backend...\n");
/* Try to get RTC capability from L4Re environment */
rtc_cap = L4Re::Env::env()->get_cap<L4rtc::Rtc>("rtc");
if (!rtc_cap.is_valid()) {
fprintf(stderr, "[L4Re Time] WARNING: RTC capability not available\n");
fprintf(stderr, "[L4Re Time] Falling back to monotonic-only mode\n");
fprintf(stderr, "[L4Re Time] Real-time will use KIP clock (time since boot)\n");
rtc_available = 0;
rtc_offset = 0;
return;
}
/* RTC capability acquired successfully */
fprintf(stderr, "[L4Re Time] RTC capability acquired\n");
/* Query the RTC timer offset (nanoseconds from epoch to boot) */
long ret = rtc_cap->get_timer_offset(&rtc_offset);
if (ret < 0) {
fprintf(stderr, "[L4Re Time] ERROR: Failed to get RTC timer offset (ret=%ld)\n", ret);
fprintf(stderr, "[L4Re Time] RTC may not be initialized or running\n");
fprintf(stderr, "[L4Re Time] Setting offset to 0 (will show time since boot as epoch)\n");
rtc_available = 0;
rtc_offset = 0;
return;
}
/* Success! */
rtc_available = 1;
fprintf(stderr, "[L4Re Time] RTC offset retrieved: %llu ns since epoch\n",
(unsigned long long) rtc_offset);
/* Convert offset to a readable date for verification */
time_t boot_time_sec = (time_t)(rtc_offset / 1000000000ULL);
struct tm *boot_tm = gmtime(&boot_time_sec);
if (boot_tm) {
char boot_str[64];
strftime(boot_str, sizeof(boot_str), "%Y-%m-%d %H:%M:%S UTC", boot_tm);
fprintf(stderr, "[L4Re Time] System boot time: %s\n", boot_str);
}
/* Display current time */
l4_kernel_info_t *kip = l4re_kip();
if (kip) {
l4_uint64_t now_ns = rtc_offset + l4_kip_clock_ns(kip);
time_t now_sec = (time_t)(now_ns / 1000000000ULL);
struct tm *now_tm = gmtime(&now_sec);
if (now_tm) {
char now_str[64];
strftime(now_str, sizeof(now_str), "%Y-%m-%d %H:%M:%S UTC", now_tm);
fprintf(stderr, "[L4Re Time] Current time: %s\n", now_str);
}
}
fprintf(stderr, "[L4Re Time] Initialization complete\n");
}
#else
/* Dummy implementation when not building for L4Re */
const sf_time_backend_t sf_time_backend_l4re = {0};
void sf_time_init_l4re(void) {
}
#endif /* __l4__ */
+4
View File
@@ -0,0 +1,4 @@
# src/platform/linux/
- `time.c` — POSIX timing backend (`clock_gettime`) for the hosted Linux
build; implements `include/platform_time.h`.
+49
View File
@@ -0,0 +1,49 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* StarForth Platform IO - Linux/POSIX Backend
*
* Implements platform_io.h's terminal-readiness check using select() on
* fd 0, the standard POSIX non-blocking-stdin-peek idiom. Zero timeout:
* a single poll, never blocks.
*
* License: See LICENSE file
*/
#include "../../../include/platform_io.h"
#include <sys/select.h>
#include <unistd.h>
int sf_terminal_ready(void) {
fd_set readfds;
struct timeval tv;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
tv.tv_sec = 0;
tv.tv_usec = 0;
int r = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
return (r > 0 && FD_ISSET(STDIN_FILENO, &readfds)) ? 1 : 0;
}
+172
View File
@@ -0,0 +1,172 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* StarForth Platform Time - Linux/POSIX Backend
*
* Implements platform time abstraction using POSIX clock_gettime().
*
* License: See LICENSE file
*/
#define _POSIX_C_SOURCE 199309L
#include "../../../include/platform_time.h"
#include <stdio.h>
#include <time.h>
#include <string.h>
/* POSIX implementation */
/**
* @brief Return the current monotonic time in nanoseconds.
*
* Reads @c CLOCK_MONOTONIC via @c clock_gettime(). The clock advances
* continuously and is unaffected by wall-clock adjustments — suitable for
* measuring elapsed durations such as execution latency and decay intervals.
* Returns 0 on @c clock_gettime() failure (should not occur on any modern
* POSIX kernel).
*
* @return Nanoseconds since an arbitrary but fixed epoch, or 0 on error
*/
static sf_time_ns_t posix_get_monotonic_ns(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
return 0;
return (sf_time_ns_t) ts.tv_sec * 1000000000ULL + (sf_time_ns_t) ts.tv_nsec;
}
/**
* @brief Return the current wall-clock time in nanoseconds since the Unix epoch.
*
* Reads @c CLOCK_REALTIME via @c clock_gettime(). Subject to NTP or @c settimeofday()
* adjustments — use @c posix_get_monotonic_ns() for interval timing.
* Returns 0 on @c clock_gettime() failure.
*
* @return Nanoseconds since 1970-01-01 00:00:00 UTC, or 0 on error
*/
static sf_time_ns_t posix_get_realtime_ns(void) {
struct timespec ts;
if (clock_gettime(CLOCK_REALTIME, &ts) != 0)
return 0;
return (sf_time_ns_t) ts.tv_sec * 1000000000ULL + (sf_time_ns_t) ts.tv_nsec;
}
/**
* @brief Set the system real-time clock to @c ns_since_epoch.
*
* Converts @c ns_since_epoch to a @c struct timespec and calls
* @c clock_settime(CLOCK_REALTIME). Requires @c CAP_SYS_TIME or equivalent
* privilege; fails with @c EPERM if the caller lacks permission.
*
* @param ns_since_epoch Desired new wall-clock time in nanoseconds since Unix epoch
* @return 0 on success, non-zero errno-encoded error on failure
*/
static int posix_set_realtime_ns(sf_time_ns_t ns_since_epoch) {
struct timespec ts;
ts.tv_sec = (time_t)(ns_since_epoch / 1000000000ULL);
ts.tv_nsec = (long) (ns_since_epoch % 1000000000ULL);
return clock_settime(CLOCK_REALTIME, &ts);
}
/**
* @brief Format a nanosecond-precision wall-clock time as a human-readable string.
*
* Converts @c ns_since_epoch to local time, formats hours/minutes/seconds via
* @c strftime(), then appends a three-digit millisecond suffix. The result is
* written into @c buf (must be at least @c SF_TIME_STAMP_SIZE bytes). On
* @c localtime() or @c strftime() failure, writes @c "??:??:??.???" into @c buf
* and returns -1.
*
* @param ns_since_epoch Wall-clock time in nanoseconds since Unix epoch
* @param buf Output buffer (at least @c SF_TIME_STAMP_SIZE bytes); must not be NULL
* @param format_24h Non-zero for 24-hour format (@c %H:%M:%S), zero for 12-hour (@c %I:%M:%S %p)
* @return 0 on success, -1 if @c buf is NULL or time formatting fails
*/
static int posix_format_timestamp(sf_time_ns_t ns_since_epoch, char *buf, int format_24h) {
if (!buf)
return -1;
time_t seconds = (time_t)(ns_since_epoch / 1000000000ULL);
unsigned int ms = (unsigned int)((ns_since_epoch % 1000000000ULL) / 1000000ULL);
struct tm *tm_info = localtime(&seconds);
if (!tm_info) {
strncpy(buf, "??:??:??.???", SF_TIME_STAMP_SIZE - 1);
buf[SF_TIME_STAMP_SIZE - 1] = '\0';
return -1;
}
char hms[16];
const char *format = format_24h ? "%H:%M:%S" : "%I:%M:%S %p";
if (strftime(hms, sizeof(hms), format, tm_info) == 0) {
strncpy(buf, "??:??:??.???", SF_TIME_STAMP_SIZE - 1);
buf[SF_TIME_STAMP_SIZE - 1] = '\0';
return -1;
}
snprintf(buf, SF_TIME_STAMP_SIZE, "%s.%03u", hms, ms);
return 0;
}
/**
* @brief Probe whether a real-time clock is available.
*
* Attempts a @c clock_gettime(CLOCK_REALTIME) probe. POSIX systems invariably
* support @c CLOCK_REALTIME so this will return 1 in practice; the check
* mirrors the bare-metal backend's interface where RTC presence is genuinely
* uncertain.
*
* @return 1 if @c CLOCK_REALTIME is readable, 0 otherwise
*/
static int posix_has_rtc(void) {
/* POSIX systems typically have working CLOCK_REALTIME */
struct timespec ts;
return (clock_gettime(CLOCK_REALTIME, &ts) == 0) ? 1 : 0;
}
/* POSIX backend vtable */
const sf_time_backend_t sf_time_backend_posix = {
.get_monotonic_ns = posix_get_monotonic_ns,
.get_realtime_ns = posix_get_realtime_ns,
.set_realtime_ns = posix_set_realtime_ns,
.format_timestamp = posix_format_timestamp,
.has_rtc = posix_has_rtc,
};
+85
View File
@@ -0,0 +1,85 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* StarForth Platform Initialization
*
* Selects and initializes the appropriate platform backend based on build configuration.
*
* License: See LICENSE file
*/
#include "../../include/platform_time.h"
/* Global backend pointer (starts as NULL) */
const sf_time_backend_t *sf_time_backend = NULL;
/* HISTORICAL: L4Re-specific init (defined in src/platform/l4re/time.c,
* retained but no longer wired into any build). L4Re support has been
* removed as an active target.
* #ifdef __l4__
* extern void sf_time_init_l4re(void);
* #endif
*/
/**
* @brief Initialize platform time subsystem
*
* Selects the POSIX backend (clock_gettime()). Call this once at program
* startup before using any time functions.
*/
void sf_time_init(void) {
if (sf_time_backend != NULL)
return; /* Already initialized */
/* HISTORICAL: an #ifdef __l4__ branch here called sf_time_init_l4re()
* and selected sf_time_backend_l4re. L4Re support has been removed as
* an active target.
* #ifdef __l4__
* sf_time_init_l4re();
* sf_time_backend = &sf_time_backend_l4re;
* #else
*/
/* POSIX build */
sf_time_backend = &sf_time_backend_posix;
/* #endif -- matches the commented-out #ifdef __l4__ above */
}
+202
View File
@@ -0,0 +1,202 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* platform/threading.c - Minimal mutex abstraction bridging POSIX and
* embedded (spinlock-based) targets
*/
#include "../../include/platform_lock.h"
/* HISTORICAL: these two conditions also referenced L4RE_TARGET
* (`&& !defined(L4RE_TARGET)` / `|| defined(L4RE_TARGET)`) -- L4Re had no
* pthreads either, so it shared the spinlock path with STARFORTH_MINIMAL.
* L4Re support has been removed as an active target. */
#if !defined(STARFORTH_MINIMAL)
#include <pthread.h>
#endif
#if defined(STARFORTH_MINIMAL)
/* Spinlock-based mutex for minimal/embedded builds */
/**
* @brief Initialise a spinlock mutex (minimal build).
*
* Sets @c mutex->state to 0 (unlocked). Safe to call on a mutex that has
* already been destroyed. On embedded targets where @c pthread is unavailable,
* this replaces @c pthread_mutex_init().
*
* @param mutex Mutex to initialise; returns -1 if NULL
* @return 0 on success, -1 if @c mutex is NULL
*/
int sf_mutex_init(sf_mutex_t *mutex)
{
if (!mutex) return -1;
mutex->state = 0;
return 0;
}
/**
* @brief Destroy a spinlock mutex (minimal build).
*
* Resets @c mutex->state to 0. No resources are released because the
* spinlock uses only the single @c state field. Safe to call on an
* already-destroyed mutex.
*
* @param mutex Mutex to destroy; no-op if NULL
*/
void sf_mutex_destroy(sf_mutex_t *mutex)
{
if (!mutex) return;
mutex->state = 0;
}
/**
* @brief Acquire a spinlock mutex (minimal build).
*
* Busy-waits using @c __sync_lock_test_and_set() until the lock is obtained.
* Issues an architecture-appropriate yield hint (@c PAUSE on x86,
* @c YIELD on ARM/AArch64, @c NOP on RISC-V) inside the spin loop to
* reduce power consumption and SMT contention.
*
* @param mutex Mutex to lock; no-op if NULL
*/
void sf_mutex_lock(sf_mutex_t *mutex)
{
if (!mutex) return;
while (__sync_lock_test_and_set(&mutex->state, 1))
{
/* CPU yield to reduce power consumption and improve performance */
#if defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)
/* x86/x86_64: PAUSE instruction reduces pipeline flush penalty */
__asm__ __volatile__("pause" ::: "memory");
#elif defined(__aarch64__) || defined(__arm__)
/* ARM/AArch64: YIELD hint for SMT threads */
__asm__ __volatile__("yield" ::: "memory");
#elif defined(__riscv)
/* RISC-V: NOP as yield hint */
__asm__ __volatile__("nop" ::: "memory");
#else
/* Fallback: compiler barrier prevents optimization */
__asm__ __volatile__("" ::: "memory");
#endif
}
}
/**
* @brief Release a spinlock mutex (minimal build).
*
* Clears the lock state atomically using @c __sync_lock_release(), which
* implies a full memory barrier on all supported architectures. Threads
* blocked in @c sf_mutex_lock() will observe the release on their next
* test-and-set iteration.
*
* @param mutex Mutex to unlock; no-op if NULL
*/
void sf_mutex_unlock(sf_mutex_t *mutex)
{
if (!mutex) return;
__sync_lock_release(&mutex->state);
}
#else /* POSIX */
/**
* @brief Initialise a POSIX mutex (hosted build).
*
* Delegates to @c pthread_mutex_init() with default attributes.
*
* @param mutex Mutex to initialise; returns -1 if NULL
* @return 0 on success, -1 if @c mutex is NULL, or a positive @c errno value
* propagated from @c pthread_mutex_init()
*/
int sf_mutex_init(sf_mutex_t *mutex)
{
if (!mutex) return -1;
return pthread_mutex_init(&mutex->handle, NULL);
}
/**
* @brief Destroy a POSIX mutex (hosted build).
*
* Delegates to @c pthread_mutex_destroy(). The mutex must not be locked
* when destroyed. Return value is discarded; callers that need the error
* code should call @c pthread_mutex_destroy() directly.
*
* @param mutex Mutex to destroy; no-op if NULL
*/
void sf_mutex_destroy(sf_mutex_t *mutex)
{
if (!mutex) return;
pthread_mutex_destroy(&mutex->handle);
}
/**
* @brief Acquire a POSIX mutex (hosted build).
*
* Delegates to @c pthread_mutex_lock(). Blocks until the mutex is available.
* Return value is discarded; a locking error will manifest as a deadlock or
* data race, not a silent failure.
*
* @param mutex Mutex to lock; no-op if NULL
*/
void sf_mutex_lock(sf_mutex_t *mutex)
{
if (!mutex) return;
pthread_mutex_lock(&mutex->handle);
}
/**
* @brief Release a POSIX mutex (hosted build).
*
* Delegates to @c pthread_mutex_unlock(). Must be called by the same thread
* that acquired the lock. Return value is discarded.
*
* @param mutex Mutex to unlock; no-op if NULL
*/
void sf_mutex_unlock(sf_mutex_t *mutex)
{
if (!mutex) return;
pthread_mutex_unlock(&mutex->handle);
}
#endif
+530
View File
@@ -0,0 +1,530 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file profiler.c
* @brief Performance profiling implementation for StarForth
*
* This module implements performance profiling functionality for the StarForth virtual machine.
* It tracks execution statistics including:
* - Word execution counts and timing
* - Dictionary lookup frequency
* - Stack operations
* - Memory usage patterns
*
* The profiler supports multiple detail levels from basic to verbose profiling.
*/
#define _POSIX_C_SOURCE 199309L
#include "../include/profiler.h"
#include "../include/platform_time.h"
#include "../include/vm.h"
#include "../include/physics_metadata.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* Forward declare DictEntry */
typedef struct DictEntry DictEntry;
/**
* @brief Statistics for individual word execution
*/
typedef struct {
const DictEntry *entry; /**< Pointer to dictionary entry */
uint64_t call_count; /**< Number of times word was called */
uint64_t total_time_ns; /**< Total execution time in nanoseconds */
uint64_t min_time_ns; /**< Minimum execution time in nanoseconds */
uint64_t max_time_ns; /**< Maximum execution time in nanoseconds */
} WordStats;
/* Global profiler state */
static struct {
ProfileLevel level;
int enabled;
/* Word execution tracking */
WordStats *word_stats;
size_t word_count;
size_t word_capacity;
/* Current word timing */
const char *current_word;
uint64_t current_start_time;
/* Global counters */
uint64_t total_dict_lookups;
uint64_t total_stack_ops;
uint64_t total_memory_reads;
uint64_t total_memory_writes;
} profiler_state = {0};
void *g_profiler = &profiler_state;
/**
* @brief Get current time in nanoseconds
* @return Current time in nanoseconds from monotonic clock
*/
static uint64_t get_time_ns(void) {
/* Use platform abstraction for monotonic time */
return sf_monotonic_ns();
}
/**
* @brief Safely print a word name, handling non-null-terminated names
* @param entry Dictionary entry whose name to print
*/
static void print_word_name_safe(const DictEntry *entry) {
if (!entry) {
printf("<null>");
return;
}
for (size_t i = 0; i < entry->name_len && i < 32; i++) {
putchar((unsigned char) entry->name[i]);
}
}
/*
* @brief Initialize the profiler subsystem
* @param level Profiling detail level to enable
* @return 0 on success, -1 on failure
*/
int profiler_init(ProfileLevel level) {
if (level == PROFILE_DISABLED) {
return 0;
}
profiler_state.level = level;
profiler_state.enabled = 1;
/* Allocate initial word stats array */
profiler_state.word_capacity = 256;
profiler_state.word_stats = calloc(profiler_state.word_capacity, sizeof(WordStats));
if (!profiler_state.word_stats) {
profiler_state.enabled = 0;
return -1;
}
profiler_state.word_count = 0;
profiler_state.total_dict_lookups = 0;
profiler_state.total_stack_ops = 0;
profiler_state.total_memory_reads = 0;
profiler_state.total_memory_writes = 0;
return 0;
}
/**
* @brief Shutdown the profiler and free resources
*/
void profiler_shutdown(void) {
if (profiler_state.word_stats) {
free(profiler_state.word_stats);
profiler_state.word_stats = NULL;
}
profiler_state.enabled = 0;
profiler_state.word_count = 0;
}
/**
* @brief Find or create statistics entry for a word
* @param entry Dictionary entry to find/create stats for
* @return Pointer to word stats or NULL if not found/created
*/
static WordStats *get_word_stats(const DictEntry *entry) {
if (!profiler_state.enabled || !entry) return NULL;
/* Search for existing entry - compare by pointer address since
DictEntry pointers are stable for the lifetime of the word */
for (size_t i = 0; i < profiler_state.word_count; i++) {
if (profiler_state.word_stats[i].entry == entry) {
return &profiler_state.word_stats[i];
}
}
/* Create new entry if space available */
if (profiler_state.word_count < profiler_state.word_capacity) {
WordStats *stats = &profiler_state.word_stats[profiler_state.word_count++];
stats->entry = entry;
stats->call_count = 0;
stats->total_time_ns = 0;
stats->min_time_ns = UINT64_MAX;
stats->max_time_ns = 0;
return stats;
}
return NULL;
}
/*
* @brief Record entry into a word for profiling
* @param entry Dictionary entry being entered
*/
void profiler_word_enter(const DictEntry *entry) {
if (!profiler_state.enabled || profiler_state.level < PROFILE_DETAILED) return;
if (!entry) return;
profiler_state.current_word = (const char *) entry;
profiler_state.current_start_time = get_time_ns();
}
/*
* @brief Record exit from a word for profiling
* @param entry Dictionary entry being exited
*/
void profiler_word_exit(const DictEntry *entry) {
if (!profiler_state.enabled || profiler_state.level < PROFILE_DETAILED) return;
if (!entry || !profiler_state.current_word) return;
uint64_t elapsed = get_time_ns() - profiler_state.current_start_time;
WordStats *stats = get_word_stats(entry);
if (stats) {
stats->call_count++;
stats->total_time_ns += elapsed;
if (elapsed < stats->min_time_ns) stats->min_time_ns = elapsed;
if (elapsed > stats->max_time_ns) stats->max_time_ns = elapsed;
}
physics_metadata_record_latency((DictEntry *) entry, elapsed);
profiler_state.current_word = NULL;
}
/*
* @brief Track word execution frequency without timing overhead
* @param entry Dictionary entry being executed
*
* This is a lightweight alternative to profiler_word_enter/exit that only
* tracks execution counts without timing. Use for PROFILE_BASIC level.
*/
void profiler_word_count(const DictEntry *entry) {
if (!profiler_state.enabled || !entry) return;
if (profiler_state.level >= PROFILE_DETAILED) return;
WordStats *stats = get_word_stats(entry);
if (stats) {
stats->call_count++;
}
}
/* Increment counters */
void profiler_inc_dict_lookup(void) {
if (profiler_state.enabled && profiler_state.level >= PROFILE_BASIC) {
profiler_state.total_dict_lookups++;
}
}
void profiler_inc_stack_op(void) {
if (profiler_state.enabled && profiler_state.level >= PROFILE_VERBOSE) {
profiler_state.total_stack_ops++;
}
}
void profiler_memory_read(size_t bytes) {
if (profiler_state.enabled && profiler_state.level >= PROFILE_VERBOSE) {
profiler_state.total_memory_reads += bytes;
}
}
void profiler_memory_write(size_t bytes) {
if (profiler_state.enabled && profiler_state.level >= PROFILE_VERBOSE) {
profiler_state.total_memory_writes += bytes;
}
}
/**
* @brief Compare two WordStats entries by total execution time
* @param a First WordStats entry to compare
* @param b Second WordStats entry to compare
* @return -1, 0, or 1 for qsort ordering
*/
static int compare_by_time(const void *a, const void *b) {
const WordStats *wa = (const WordStats *) a;
const WordStats *wb = (const WordStats *) b;
if (wb->total_time_ns > wa->total_time_ns) return 1;
if (wb->total_time_ns < wa->total_time_ns) return -1;
return 0;
}
/**
* @brief Compare two WordStats entries by call count
* @param a First WordStats entry to compare
* @param b Second WordStats entry to compare
* @return -1, 0, or 1 for qsort ordering
*/
static int compare_by_calls(const void *a, const void *b) {
const WordStats *wa = (const WordStats *) a;
const WordStats *wb = (const WordStats *) b;
if (wb->call_count > wa->call_count) return 1;
if (wb->call_count < wa->call_count) return -1;
return 0;
}
/**
* @brief Generate and print detailed profiling report
*
* Generates a report including:
* - Global statistics (dictionary lookups, stack ops, memory usage)
* - Top words by execution time
* - Most frequently called words
*/
void profiler_generate_report(void) {
if (!profiler_state.enabled) {
printf("Profiler not enabled\n");
return;
}
printf("\n");
printf("========================================\n");
printf(" StarForth Profiler Report\n");
printf("========================================\n\n");
/* Global statistics */
if (profiler_state.level >= PROFILE_BASIC) {
printf("Global Statistics:\n");
printf(" Dictionary lookups: %lu\n", (unsigned long) profiler_state.total_dict_lookups);
if (profiler_state.level >= PROFILE_VERBOSE) {
printf(" Stack operations: %lu\n", (unsigned long) profiler_state.total_stack_ops);
printf(" Memory reads: %lu bytes\n", (unsigned long) profiler_state.total_memory_reads);
printf(" Memory writes: %lu bytes\n", (unsigned long) profiler_state.total_memory_writes);
}
printf("\n");
}
/* Word execution statistics */
if (profiler_state.word_count > 0) {
if (profiler_state.level >= PROFILE_DETAILED) {
/* Detailed mode: show timing info */
printf("Top Words by Execution Time:\n");
printf("%-20s %10s %15s %12s %12s\n", "Word", "Calls", "Total (µs)", "Avg (ns)", "Max (ns)");
printf("--------------------------------------------------------------------------------\n");
/* Sort by total time */
qsort(profiler_state.word_stats, profiler_state.word_count,
sizeof(WordStats), compare_by_time);
/* Show top 20 */
size_t limit = profiler_state.word_count < 20 ? profiler_state.word_count : 20;
for (size_t i = 0; i < limit; i++) {
WordStats *s = &profiler_state.word_stats[i];
if (s->call_count > 0) {
uint64_t avg_ns = s->total_time_ns / s->call_count;
print_word_name_safe(s->entry);
/* Pad to 20 characters */
size_t name_len = s->entry ? s->entry->name_len : 0;
for (size_t j = name_len; j < 20; j++) putchar(' ');
printf(" %10lu %15.2f %12lu %12lu\n",
(unsigned long) s->call_count,
s->total_time_ns / 1000.0,
(unsigned long) avg_ns,
(unsigned long) s->max_time_ns);
}
}
printf("\n");
}
/* Show frequency data for all levels >= BASIC */
if (profiler_state.level >= PROFILE_BASIC) {
printf("Most Frequently Called Words:\n");
printf("%-20s %10s\n", "Word", "Calls");
printf("----------------------------------------\n");
qsort(profiler_state.word_stats, profiler_state.word_count,
sizeof(WordStats), compare_by_calls);
size_t limit = profiler_state.word_count < 15 ? profiler_state.word_count : 15;
for (size_t i = 0; i < limit; i++) {
WordStats *s = &profiler_state.word_stats[i];
if (s->call_count > 0) {
print_word_name_safe(s->entry);
/* Pad to 20 characters */
size_t name_len = s->entry ? s->entry->name_len : 0;
for (size_t j = name_len; j < 20; j++) putchar(' ');
printf(" %10lu\n", (unsigned long) s->call_count);
}
}
printf("\n");
}
}
printf("========================================\n\n");
}
/* Stub implementations for unused functions */
void profiler_reset(void) {
if (!profiler_state.enabled) return;
/* Reset all counters */
for (size_t i = 0; i < profiler_state.word_count; i++) {
profiler_state.word_stats[i].call_count = 0;
profiler_state.word_stats[i].total_time_ns = 0;
profiler_state.word_stats[i].min_time_ns = UINT64_MAX;
profiler_state.word_stats[i].max_time_ns = 0;
}
profiler_state.total_dict_lookups = 0;
profiler_state.total_stack_ops = 0;
profiler_state.total_memory_reads = 0;
profiler_state.total_memory_writes = 0;
}
void profiler_set_level(ProfileLevel level) {
profiler_state.level = level;
}
uint64_t profiler_get_time_ns(void) {
return get_time_ns();
}
void profiler_start_timer(ProfileCategory category) { (void) category; }
void profiler_end_timer(ProfileCategory category) { (void) category; }
void profiler_memory_alloc(size_t bytes) { (void) bytes; }
void profiler_inc_vm_cycles(uint64_t cycles) { (void) cycles; }
void profiler_inc_branch(void) {
}
void profiler_inc_loop_iter(void) {
}
void profiler_print_summary(void) { profiler_generate_report(); }
/**
* @brief Print hot word analysis with optimization recommendations
*
* Analyzes word execution frequency and provides actionable recommendations
* for performance optimization including:
* - Assembly optimization candidates
* - Inlining opportunities
* - Cache-friendly ordering
*/
void profiler_print_hotspots(void) {
if (!profiler_state.enabled || profiler_state.word_count == 0) {
printf("No profiling data available\n");
return;
}
printf("\n");
printf("========================================\n");
printf(" Hot Word Analysis & Optimization\n");
printf("========================================\n\n");
/* Sort by call frequency */
qsort(profiler_state.word_stats, profiler_state.word_count,
sizeof(WordStats), compare_by_calls);
/* Calculate total calls for percentage */
uint64_t total_calls = 0;
for (size_t i = 0; i < profiler_state.word_count; i++) {
total_calls += profiler_state.word_stats[i].call_count;
}
if (total_calls == 0) {
printf("No word executions recorded\n");
return;
}
printf("Top 25 Most Frequently Called Words:\n");
printf("%-20s %12s %10s %s\n", "Word", "Calls", "% Total", "Optimization Suggestion");
printf("--------------------------------------------------------------------------------\n");
size_t limit = profiler_state.word_count < 25 ? profiler_state.word_count : 25;
for (size_t i = 0; i < limit; i++) {
WordStats *s = &profiler_state.word_stats[i];
if (s->call_count == 0) continue;
double pct = (s->call_count * 100.0) / total_calls;
/* Print word name */
print_word_name_safe(s->entry);
size_t name_len = s->entry ? s->entry->name_len : 0;
for (size_t j = name_len; j < 20; j++) putchar(' ');
printf(" %12lu %9.2f%% ", (unsigned long) s->call_count, pct);
/* Provide optimization suggestions based on frequency */
if (pct >= 5.0) {
printf("⚡ HIGH PRIORITY: Inline assembly candidate");
} else if (pct >= 2.0) {
printf("🔥 Consider assembly optimization");
} else if (pct >= 1.0) {
printf("💡 Monitor for optimization");
} else if (pct >= 0.5) {
printf("📊 Profile for PGO");
} else {
printf("—");
}
printf("\n");
}
printf("\n");
printf("Summary:\n");
printf(" Total word executions: %lu\n", (unsigned long) total_calls);
printf(" Unique words tracked: %lu\n", (unsigned long) profiler_state.word_count);
/* Calculate top 10 coverage */
uint64_t top10_calls = 0;
size_t top10_limit = profiler_state.word_count < 10 ? profiler_state.word_count : 10;
for (size_t i = 0; i < top10_limit; i++) {
top10_calls += profiler_state.word_stats[i].call_count;
}
double top10_pct = (top10_calls * 100.0) / total_calls;
printf(" Top 10 coverage: %.1f%%\n", top10_pct);
printf("\n========================================\n\n");
}
void profiler_print_word_analysis(void) {
profiler_print_hotspots();
}
void profiler_print_memory_analysis(void) {
}
void profiler_analyze_performance(VM *vm) { (void) vm; }
void profiler_suggest_optimizations(void) {
profiler_print_hotspots();
}
+144
View File
@@ -0,0 +1,144 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/repl.h"
#include "../include/log.h"
#include "../include/vm.h"
#include <stdio.h>
/**
* @brief Extension point for non-recoverable VM errors (weak symbol).
*
* Called by the REPL when @c EMERGENCY_CONSOLE_ENABLED = 0 and an error
* occurs. The default implementation logs the fault at @c LOG_ERROR level
* and sets @c vm->halted = 1, causing the REPL loop to exit cleanly.
*
* Platforms that need hardware reset, watchdog kick, or JTAG debug-probe
* integration should provide a strong definition in a platform-specific file
* (e.g., @c starkernel/hal/fault.c) to override this weak default.
*
* @param vm Active VM whose @c vm->error triggered the fault path
*/
__attribute__((weak)) void vm_fault_handler(VM *vm) {
log_message(LOG_ERROR, "VM fault — emergency console disabled; halting");
vm->halted = 1;
}
/**
* @brief Starts the Forth REPL (Read-Eval-Print Loop)
*
* @param vm Pointer to the VM structure
* @param script_mode If 1, suppress prompts and "ok" output (for piped input)
*
* @details This function implements an interactive REPL for the Forth interpreter.
* It continuously reads input from the user, interprets it, and prints the result
* until the VM is halted or an error occurs.
*
* In interactive mode (script_mode=0):
* - Prints colored prompt "ok> "
* - Acknowledges successful commands with " ok"
* - Reports errors with " ERROR"
*
* In script mode (script_mode=1):
* - Suppresses all prompts and status messages
* - Suitable for piped input (heredoc, pipes, redirects)
*/
void vm_repl(VM *vm, int script_mode) {
if (!script_mode) {
log_message(LOG_INFO, "Starting Forth REPL");
}
char input[256];
while (!vm->halted && !vm->error) {
/* Print prompt only in interactive mode */
if (!script_mode) {
if (vm->zuse_session)
printf("\033[36mzuse)ok>\033[0m \033[92m");
else
printf("\033[36mok>\033[0m \033[92m");
fflush(stdout);
}
if (!fgets(input, sizeof(input), stdin)) {
if (!script_mode) {
printf("\033[0m");
fflush(stdout);
}
break;
}
if (!script_mode) {
printf("\033[0m");
fflush(stdout);
}
vm_interpret(vm, input);
/* ABORT stops mid-line (see vm_interpret/execute_colon_word) but the
* flag is left set for the caller to consume -- this REPL line is
* that boundary. Clear it here so the next line isn't silently
* refused by vm_interpret's own abort_requested check. */
vm->abort_requested = 0;
if (!script_mode) {
/* Print status only in interactive mode */
if (!vm->error) {
printf(" ok\n");
} else {
printf(" ERROR\n");
#if EMERGENCY_CONSOLE_ENABLED
vm->error = 0; /* recover: allow next input */
#else
vm_fault_handler(vm); /* non-recoverable: extension point */
#endif
}
} else {
if (vm->error) {
#if EMERGENCY_CONSOLE_ENABLED
vm->error = 0;
#else
vm_fault_handler(vm);
#endif
}
}
}
}
File diff suppressed because it is too large Load Diff
+482
View File
@@ -0,0 +1,482 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "ssm_jacquard.h"
#include <string.h>
#include <stdlib.h>
/* ============================================================================
* L8 Initialization
* ============================================================================
*/
/**
* @brief Initialise the L8 Jacquard steady-state machine.
*
* Sets @c current_mode and @c pending_mode to @c initial_mode, zeroes
* @c hysteresis_counter, and sets @c table to NULL. Callers must invoke
* @c ssm_l8_init_table() separately if the adaptive UCB table is required.
*
* @param state Pointer to the L8 state to initialise
* @param initial_mode Starting mode (typically @c SSM_MODE_C0)
*/
void ssm_l8_init(ssm_l8_state_t *state, ssm_l8_mode_t initial_mode)
{
if (!state) return;
state->current_mode = initial_mode;
state->hysteresis_counter = 0;
state->pending_mode = initial_mode;
state->table = NULL; /* Caller calls ssm_l8_init_table() separately */
}
/* ============================================================================
* L8 Mode Selection Logic (Data-Driven from Top 5% DoE Analysis)
* ============================================================================
*/
/**
* @brief Update the L8 mode selector using current runtime metrics.
*
* Classifies @c metrics into four binary feature bits (L2/L3/L5/L6) derived
* from DoE top-5% analysis and computes a target mode. Applies hysteresis:
* the mode only commits after @c SSM_HYSTERESIS_TICKS consecutive ticks with
* the same target. Called by the heartbeat thread on every tick.
*
* @param metrics Runtime metrics snapshot (entropy, CV, temporal decay)
* @param state L8 state to update; @c current_mode is written on commit
*/
void ssm_l8_update(const ssm_l8_metrics_t *metrics, ssm_l8_state_t *state)
{
if (!metrics || !state) return;
/* Classify metrics into binary features */
int entropy_high = (metrics->entropy >= SSM_ENTROPY_HIGH_THRESHOLD);
int cv_high = (metrics->cv >= SSM_CV_HIGH_THRESHOLD);
int temporal_high = (metrics->temporal_decay >= SSM_TEMPORAL_DECAY_THRESHOLD);
int temporal_med = (metrics->temporal_decay >= SSM_TEMPORAL_DECAY_LOW_THRESHOLD);
/* Determine target mode using 4-bit logic (L2 L3 L5 L6) */
ssm_l8_mode_t target_mode = SSM_MODE_C0; /* Default: minimal */
/* L2 bit: Enable rolling window if high entropy (diversity) */
int L2_bit = entropy_high ? 1 : 0;
/* L3 bit: Enable linear decay if high temporal locality */
int L3_bit = temporal_high ? 1 : 0;
/* L5 bit: Enable window inference if high CV (volatility) */
int L5_bit = cv_high ? 1 : 0;
/* L6 bit: Enable decay inference if CV high AND moderate temporal */
int L6_bit = (cv_high && temporal_med) ? 1 : 0;
/* Combine bits into mode selector: L2 L3 L5 L6 */
target_mode = (ssm_l8_mode_t)((L2_bit << 3) | (L3_bit << 2) | (L5_bit << 1) | L6_bit);
/* Hysteresis: only change mode after SSM_HYSTERESIS_TICKS consecutive votes */
if (target_mode == state->pending_mode) {
/* Same target as last time, increment counter */
state->hysteresis_counter++;
if (state->hysteresis_counter >= SSM_HYSTERESIS_TICKS) {
/* Threshold reached, commit mode change */
if (target_mode != state->current_mode) {
state->current_mode = target_mode;
}
state->hysteresis_counter = 0; /* Reset for next change */
}
} else {
/* Target changed, reset hysteresis */
state->pending_mode = target_mode;
state->hysteresis_counter = 1;
}
}
/* ============================================================================
* L8 Mode Application (Set L2/L3/L5/L6 Bits)
* ============================================================================
*/
/**
* @brief Apply the current L8 mode to a runtime configuration struct.
*
* Extracts the four loop-enable bits (L2/L3/L5/L6) from
* @c state->current_mode and writes them into @c config. Used by the
* legacy (non-table) L8 path when the adaptive UCB table is not present.
*
* @param state L8 state containing the committed current mode
* @param config Runtime configuration to update
*/
void ssm_apply_mode(const ssm_l8_state_t *state, ssm_config_t *config)
{
if (!state || !config) return;
/* Extract bits from mode (L2 L3 L5 L6) */
int mode_val = (int)state->current_mode;
config->L2_rolling_window = (mode_val >> 3) & 1; /* Bit 3 */
config->L3_linear_decay = (mode_val >> 2) & 1; /* Bit 2 */
config->L5_window_inference = (mode_val >> 1) & 1; /* Bit 1 */
config->L6_decay_inference = (mode_val >> 0) & 1; /* Bit 0 */
}
/* ============================================================================
* L8 Utility Functions
* ============================================================================
*/
/**
* @brief Return a human-readable name string for the given L8 mode.
*
* Returns a static string such as @c "C4_TEMPORAL" or @c "C15_FULL_ADAPTIVE".
* Modes marked ✅ TOP 5% are the configurations favoured by the DoE study.
* Returns @c "UNKNOWN" for out-of-range values.
*
* @param mode L8 mode enumeration value
* @return Pointer to a static null-terminated string (never NULL)
*/
const char* ssm_l8_mode_name(ssm_l8_mode_t mode)
{
switch (mode) {
case SSM_MODE_C0: return "C0_MINIMAL";
case SSM_MODE_C1: return "C1_DECAY_INF";
case SSM_MODE_C2: return "C2_WINDOW_INF";
case SSM_MODE_C3: return "C3_VOLATILE";
case SSM_MODE_C4: return "C4_TEMPORAL"; /* ✅ TOP 5% */
case SSM_MODE_C5: return "C5_TEMPORAL_DECAY_INF";
case SSM_MODE_C6: return "C6_TEMPORAL_WINDOW_INF";
case SSM_MODE_C7: return "C7_FULL_INFERENCE"; /* ✅ TOP 5% */
case SSM_MODE_C8: return "C8_DIVERSE";
case SSM_MODE_C9: return "C9_DIVERSE_DECAY_INF"; /* ✅ TOP 5% */
case SSM_MODE_C10: return "C10_DIVERSE_WINDOW_INF";
case SSM_MODE_C11: return "C11_DIVERSE_INFERENCE"; /* ✅ TOP 5% */
case SSM_MODE_C12: return "C12_DIVERSE_TEMPORAL"; /* ✅ TOP 5% */
case SSM_MODE_C13: return "C13_COMPLEX";
case SSM_MODE_C14: return "C14_FULL_ADAPTIVE_NO_DECAY_INF";
case SSM_MODE_C15: return "C15_FULL_ADAPTIVE";
default: return "UNKNOWN";
}
}
/* ============================================================================
* Adaptive Table: DoE Prior Seeding
* ============================================================================
*
* Maps the DoE top-5% 4-bit modes (L2/L3/L5/L6) to the 7-bit space
* (L1=0, L4=0, L7=1 held fixed for the high-score variants).
*
* Bit layout: b6=L1, b5=L2, b4=L3, b3=L4, b2=L5, b1=L6, b0=L7
*
* Top-5% modes (4-bit → 7-bit with L1=0,L4=0,L7=1):
* C4 (L3 only): 0100 → 0 0 1 0 0 0 1 = 0x11 = 17
* C7 (L3+L5+L6): 0111 → 0 0 1 0 1 1 1 = 0x17 = 23
* C9 (L2+L6): 1001 → 0 1 0 0 0 1 1 = 0x23 = 35
* C11 (L2+L5+L6): 1011 → 0 1 0 0 1 1 1 = 0x27 = 39
* C12 (L2+L3): 1100 → 0 1 1 0 0 0 1 = 0x31 = 49
*/
/**
* @brief Seed the adaptive UCB config table with DoE prior scores.
*
* Computes benefit scores derived from the DoE top-5% analysis for all
* @c cd_tuning_word().num_configs (128) configs and hands them to
* @c cd_config_table_seed(). The five top-5% configs (C4/C7/C9/C11/C12 in
* 4-bit space → indices 17/23/35/39/49 in 7-bit space) receive 80% of
* @c cd_tuning_word().score_max; configs involving L1 or L4 (known expensive
* loops) receive 5–10%; all others receive 40%. Sets the starting config
* to index 17 (C4-equivalent: L3+L7 only).
*
* @param table Pointer to the CDConfigTable to seed (already initialised
* via cd_config_table_init())
*/
static void ssm_l8_seed_from_doe(CDConfigTable *table)
{
/* Top-5% config indices in 7-bit space */
static const uint8_t top5[] = { 17u, 23u, 35u, 39u, 49u };
CDTuning tuning = cd_tuning_word();
uint32_t initial_scores[128];
uint32_t score_max = tuning.score_max;
uint32_t c, i;
for (c = 0u; c < tuning.num_configs; c++) {
uint8_t bits = (uint8_t)c;
int has_l1 = (bits & SSM_CFG_L1) != 0;
int has_l4 = (bits & SSM_CFG_L4) != 0;
int is_top5 = 0;
uint32_t score;
for (i = 0u; i < 5u; i++) {
if (bits == top5[i]) { is_top5 = 1; break; }
}
if (is_top5) score = (score_max * 80u) / 100u;
else if (has_l1 && has_l4) score = (score_max * 5u) / 100u;
else if (has_l1 || has_l4) score = (score_max * 10u) / 100u;
else score = (score_max * 40u) / 100u;
initial_scores[c] = score;
}
/* Start on C4-equivalent (config index 17: L3+L7 only) */
cd_config_table_seed(table, &tuning, initial_scores, 17u);
}
/* ============================================================================
* Adaptive Table: Per-Tick Reward + UCB Selection (static)
* ============================================================================ */
/**
* @brief Score the current config from this tick's outcome and UCB-select
* this tick's config.
*
* Computes a joint convergence signal from this tick's ANOVA outcome and
* the relative delta between this tick's and the previous tick's (window,
* locality) pair. Updates the regime-specific benefit score for the current
* config using exponential decay plus a signed delta -- the decay is what
* provides smoothing over time; there's no separate multi-tick batching on
* top of it (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev r: one clock, the
* heartbeat tick -- a tick is a tick). Then runs
* UCB1 over all 128 configs for the current regime, selecting the one with
* the highest upper confidence bound as this tick's config.
*
* @param table Adaptive config table
* @param config Runtime config to update with the newly selected bits
* @param metrics This tick's metrics snapshot (for the ANOVA signal)
* @param current_window Current rolling window width, this tick
* @param locality_q16 Current locality signal from cd_classify_ids(), this tick
*/
static void ssm_l8_tick_score_and_select(CDConfigTable *table, ssm_config_t *config,
const ssm_l8_metrics_t *metrics,
uint32_t current_window, uint32_t locality_q16)
{
CDTuning tuning = cd_tuning_word();
uint8_t active_bits = table->entries[table->current_config].config_bits;
int use_joint_convergence = ((active_bits & SSM_CFG_L5) || (active_bits & SSM_CFG_L6)) ? 1 : 0;
/* --- ANOVA stability: this tick's own outcome, not an accumulated
* fraction. Neutral (32768) when inference didn't run this tick --
* no signal either way, not "unstable." */
uint32_t anova_stability = metrics->inference_ran_this_tick
? (metrics->inference_early_exited ? 65536u : 0u)
: 32768u;
uint8_t bits = cd_config_table_tick(table, &tuning, table->current_regime,
anova_stability, use_joint_convergence,
current_window, locality_q16);
/* Apply new config bits to ssm_config_t -- all 7, not just the 4
* the legacy 16-mode selector could express. */
if (config != NULL) {
config->L1_heat_tracking = (bits & SSM_CFG_L1) ? 1 : 0;
config->L2_rolling_window = (bits & SSM_CFG_L2) ? 1 : 0;
config->L3_linear_decay = (bits & SSM_CFG_L3) ? 1 : 0;
config->L4_pipelining = (bits & SSM_CFG_L4) ? 1 : 0;
config->L5_window_inference = (bits & SSM_CFG_L5) ? 1 : 0;
config->L6_decay_inference = (bits & SSM_CFG_L6) ? 1 : 0;
config->L7_adaptive_heartrate = (bits & SSM_CFG_L7) ? 1 : 0;
}
}
/* ============================================================================
* Adaptive Table: Public API
* ============================================================================
*/
/**
* @brief Allocate and initialise the adaptive UCB config table.
*
* Mallocs and initialises a @c CDConfigTable sized by @c cd_tuning_word(),
* seeds it with @c ssm_l8_seed_from_doe(), and attaches it to
* @c state->table. If either allocation fails, @c state->table is set to
* NULL and the system falls back to legacy mode (@c ssm_apply_mode()).
*
* @param state L8 state that will own the table
*/
void ssm_l8_init_table(ssm_l8_state_t *state)
{
CDConfigTable *table;
CDTuning tuning;
if (!state) return;
table = (CDConfigTable *)malloc(sizeof(CDConfigTable));
if (!table) {
state->table = NULL; /* Fall back to legacy mode */
return;
}
tuning = cd_tuning_word();
if (cd_config_table_init(table, &tuning) != 0) {
free(table);
state->table = NULL; /* Fall back to legacy mode */
return;
}
ssm_l8_seed_from_doe(table);
state->table = table;
}
/**
* @brief Free the adaptive UCB config table owned by @c state.
*
* Safe to call with @c state->table == NULL. Sets @c state->table to NULL
* after freeing so the state falls back to legacy mode.
*
* @param state L8 state whose table is freed
*/
void ssm_l8_free_table(ssm_l8_state_t *state)
{
if (!state || !state->table) return;
cd_config_table_free(state->table);
free(state->table);
state->table = NULL;
}
/**
* @brief Tick the adaptive UCB table: classify, score, and reselect.
*
* Classifies the current tick into one of 8 regimes (entropy x CV x
* locality) and stores it unconditionally, then scores the current config
* from this tick's outcome and UCB-reselects, every single call -- no
* batching, no derived counter of any kind (VM-FLEET-ATTRACTOR-DESIGN-
* 20260705.md rev r: one clock, the heartbeat tick, applied uniformly).
* The locality component of both the regime and the joint-convergence
* reward comes from cd_classify_ids() over recent_word_ids -- purely
* execution-count-derived, no wall-clock input (rev s; see header comment
* on this function's declaration for why that matters). Also mirrors the
* winning L2/L3/L5/L6 bits back into @c state->current_mode for
* diagnostic display. No-op if @c state->table is NULL.
*
* @param state L8 state with attached adaptive table
* @param metrics Current runtime metrics snapshot
* @param config Runtime config updated this tick
* @param current_window Current rolling window width
* @param recent_word_ids Recent word-execution-ID history, oldest first
* @param recent_word_ids_count Number of valid entries in recent_word_ids
*/
void ssm_l8_update_table(ssm_l8_state_t *state, const ssm_l8_metrics_t *metrics,
ssm_config_t *config,
uint32_t current_window,
const uint32_t *recent_word_ids, uint32_t recent_word_ids_count)
{
CDConfigTable *table;
CDTuning tuning = cd_tuning_word();
CDRegimeMetrics cd;
uint8_t regime;
if (!state || !state->table || !metrics) return;
table = state->table;
cd = cd_classify_ids(recent_word_ids, recent_word_ids_count,
tuning.locality_lookback);
/* Current regime (same 3-bit shape as legacy L8), stored every tick,
* unconditionally. */
regime = (uint8_t)(
((metrics->entropy >= SSM_ENTROPY_HIGH_THRESHOLD) ? 4u : 0u) |
((metrics->cv >= SSM_CV_HIGH_THRESHOLD) ? 2u : 0u) |
((cd.locality_q16 >= tuning.locality_high_threshold_q16) ? 1u : 0u)
);
table->current_regime = regime;
ssm_l8_tick_score_and_select(table, config, metrics, current_window, cd.locality_q16);
/* Mirror L2/L3/L5/L6 bits into the legacy mode field for diagnostics */
{
uint8_t bits = table->entries[table->current_config].config_bits;
state->current_mode = (ssm_l8_mode_t)(
(((bits & SSM_CFG_L2) ? 1u : 0u) << 3) |
(((bits & SSM_CFG_L3) ? 1u : 0u) << 2) |
(((bits & SSM_CFG_L5) ? 1u : 0u) << 1) |
(((bits & SSM_CFG_L6) ? 1u : 0u) << 0)
);
}
}
/**
* @brief Apply the UCB-selected config bits to a runtime configuration struct.
*
* Reads the current winning config entry from the adaptive table and writes
* all 7 of its bits into @c config. Use this instead of @c ssm_apply_mode()
* when the adaptive table is present (@c state->table != NULL).
*
* @param state L8 state with an attached, initialised adaptive table
* @param config Runtime configuration to update
*/
void ssm_apply_mode_from_table(const ssm_l8_state_t *state, ssm_config_t *config)
{
const CDConfigTable *table;
uint8_t bits;
if (!state || !state->table || !config) return;
table = state->table;
bits = table->entries[table->current_config].config_bits;
config->L1_heat_tracking = (bits & SSM_CFG_L1) ? 1 : 0;
config->L2_rolling_window = (bits & SSM_CFG_L2) ? 1 : 0;
config->L3_linear_decay = (bits & SSM_CFG_L3) ? 1 : 0;
config->L4_pipelining = (bits & SSM_CFG_L4) ? 1 : 0;
config->L5_window_inference = (bits & SSM_CFG_L5) ? 1 : 0;
config->L6_decay_inference = (bits & SSM_CFG_L6) ? 1 : 0;
config->L7_adaptive_heartrate = (bits & SSM_CFG_L7) ? 1 : 0;
}
/**
* @brief Force the adaptive table onto an externally-chosen config.
*
* Sets @c current_config directly (as if the bandit's own UCB selection had
* picked it) and applies its bits immediately. Since scoring and UCB
* reselection now run on every heartbeat tick (rev r: one clock, no
* batching of any kind), this is a one-tick nudge, not a hold: the
* forced config earns exactly one tick's worth of reward signal into its
* own regime_score, then the very next tick's ssm_l8_update_table() call
* scores it and reselects on its own, same as any other tick. Calling
* this repeatedly (e.g. once per DoE draw, faster than the VM's own
* tick cadence) keeps a config in effect in practice, not because it's
* pinned -- because it's what's currently forced, most recently.
*
* @param state L8 state with an attached, initialised adaptive table
* @param config Runtime configuration to update immediately
* @param config_idx Desired config, masked to the 128-entry (7-bit) space
*/
void ssm_l8_force_config(ssm_l8_state_t *state, ssm_config_t *config, uint8_t config_idx)
{
if (!state || !state->table || !config) return;
cd_config_table_force(state->table, config_idx & 0x7Fu);
ssm_apply_mode_from_table(state, config);
}
+177
View File
@@ -0,0 +1,177 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file stack_management.c
* @brief Implementation of stack operations for the Forth virtual machine
*
* This module provides the core stack manipulation functions for both the data stack
* and return stack of the Forth virtual machine. It includes optimized assembly
* implementations for x86_64 and ARM64 architectures when available.
*/
#include "vm.h"
#include "log.h"
#include "profiler.h"
#if defined(USE_ASM_OPT) && USE_ASM_OPT
# if defined(ARCH_X86_64)
# include "vm_asm_opt.h"
# elif defined(ARCH_ARM64) || defined(__aarch64__) || defined(__arm64__)
# include "vm_asm_opt_arm64.h"
# elif defined(ARCH_RISCV64) || (defined(__riscv) && (__riscv_xlen == 64))
# include "vm_asm_opt_riscv64.h"
# endif
#endif
/**
* @brief Push a value onto the data stack
*
* @param vm Pointer to the VM structure
* @param value Value to push onto the stack
*
* @note Performs bounds checking to prevent stack overflow
* @note Uses assembly optimization if available for x86_64 or ARM64
*/
void vm_push(VM *vm, cell_t value) {
PROFILE_INC_STACK_OP();
#if defined(USE_ASM_OPT) && USE_ASM_OPT && (defined(ARCH_X86_64) || defined(ARCH_ARM64) || defined(ARCH_RISCV64))
/* Use optimized assembly version for x86_64 or ARM64 */
vm_push_asm(vm, value);
#ifndef NDEBUG
if (!vm->error) {
log_message(LOG_DEBUG, "PUSH: %ld (dsp=%d)", value, vm->dsp);
}
#endif
#else
/* Standard C implementation */
if (vm->dsp >= STACK_SIZE - 1) {
log_message(LOG_ERROR, "Stack overflow");
vm->error = 1;
return;
}
vm->data_stack[++vm->dsp] = value;
log_message(LOG_DEBUG, "PUSH: %ld (dsp=%d)", value, vm->dsp);
#endif
}
/**
* @brief Pop a value from the data stack
*
* @param vm Pointer to the VM structure
* @return The popped value
*
* @note Returns 0 and sets error flag if stack underflow occurs
* @note Uses assembly optimization if available for x86_64 or ARM64
*/
cell_t vm_pop(VM *vm) {
PROFILE_INC_STACK_OP();
#if defined(USE_ASM_OPT) && USE_ASM_OPT && (defined(ARCH_X86_64) || defined(ARCH_ARM64) || defined(ARCH_RISCV64))
/* Use optimized assembly version for x86_64 */
cell_t value = vm_pop_asm(vm);
#ifndef NDEBUG
if (!vm->error) {
log_message(LOG_DEBUG, "POP: %ld (dsp=%d)", value, vm->dsp);
}
#endif
return value;
#else
/* Standard C implementation */
if (vm->dsp < 0) {
log_message(LOG_ERROR, "Stack underflow (dsp=%d)", vm->dsp);
vm->error = 1;
return 0;
}
cell_t value = vm->data_stack[vm->dsp--];
log_message(LOG_DEBUG, "POP: %ld (dsp=%d)", value, vm->dsp);
return value;
#endif
}
/**
* @brief Push a value onto the return stack
*
* @param vm Pointer to the VM structure
* @param value Value to push onto the return stack
*
* @note Sets error flag if stack overflow occurs
* @note Uses assembly optimization if available for x86_64 or ARM64
*/
void vm_rpush(VM *vm, cell_t value) {
#if defined(USE_ASM_OPT) && USE_ASM_OPT && (defined(ARCH_X86_64) || defined(ARCH_ARM64) || defined(ARCH_RISCV64))
/* Use optimized assembly version for x86_64 */
vm_rpush_asm(vm, value);
#else
/* Standard C implementation */
if (vm->rsp >= STACK_SIZE - 1) {
vm->error = 1; /* Return stack overflow */
return;
}
vm->return_stack[++vm->rsp] = value;
#endif
}
/**
* @brief Pop a value from the return stack
*
* @param vm Pointer to the VM structure
* @return The popped value
*
* @note Returns 0 and sets error flag if stack underflow occurs
* @note Uses assembly optimization if available for x86_64 or ARM64
*/
cell_t vm_rpop(VM *vm) {
#if defined(USE_ASM_OPT) && USE_ASM_OPT && (defined(ARCH_X86_64) || defined(ARCH_ARM64) || defined(ARCH_RISCV64))
/* Use optimized assembly version for x86_64 */
return vm_rpop_asm(vm);
#else
/* Standard C implementation */
if (vm->rsp < 0) {
vm->error = 1; /* Return stack underflow */
return 0;
}
cell_t value = vm->return_stack[vm->rsp--];
return value;
#endif
}
+17
View File
@@ -0,0 +1,17 @@
# src/test_runner/
Test harness for the hosted VM's test suite, run via
`./starforth --run-tests`. Organized in POST (Power-On Self Test) order:
unit tests → dictionary/word-category tests → integration/stress/
adversarial tests.
- `test_runner.c` — test harness orchestration (loads and runs all
modules in POST order).
- `test_common.c` — shared test utilities.
- `test_contracts.c` — contract-based testing infrastructure (pre/post
condition assertions around word execution).
Subdirectories:
- `include/` — public headers for this harness.
- `modules/` — per-category test files; see
`src/test_runner/modules/README.md`.
+8
View File
@@ -0,0 +1,8 @@
# src/test_runner/include/
Public headers for the test harness.
- `test_runner.h` — harness orchestration interface.
- `test_common.h` — shared test utility declarations.
- `test_contracts.h` — contract-based testing (pre/post-condition
assertion) interface.
+214
View File
@@ -0,0 +1,214 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file test_common.h
* @brief Common test utilities and structures for StarForth test runner
* @details This file contains test configuration, structures, and utility functions
* used throughout the StarForth test framework.
*/
#ifndef TEST_COMMON_H
#define TEST_COMMON_H
#include "../../../include/vm.h"
#include "../../../include/log.h"
#include "test_contracts.h"
/** @name Test Configuration
* @{
*/
/** Maximum length of test output buffer */
#define MAX_TEST_OUTPUT 1024
/** Maximum length of test input buffer */
#define MAX_TEST_INPUT 256
/** Maximum number of test cases per word */
#define MAX_TESTS_PER_WORD 20
/** @} */
/* fail fast */
extern int fail_fast;
/** @brief Types of test cases */
typedef enum {
TEST_NORMAL = 0, /**< Standard test case */
TEST_EDGE_CASE = 1, /**< Edge case test */
TEST_ERROR_CASE = 2 /**< Error condition test */
} TestType;
/** @brief Structure representing a single test case */
typedef struct {
const char *name; /**< Name of the test case */
const char *input; /**< Input Forth code */
const char *expected; /**< Expected output */
TestType type; /**< Type of test case */
int should_error; /**< Whether test should produce an error */
int implemented; /**< Whether the functionality is implemented */
WordContract contract; /**< Per-test contract override; zero = inherit from suite/module */
} TestCase;
/** @brief Collection of test cases for a single Forth word */
typedef struct {
const char *word_name; /**< Name of the Forth word being tested */
TestCase tests[MAX_TESTS_PER_WORD]; /**< Array of test cases */
int test_count; /**< Number of tests in the suite */
WordContract suite_contract; /**< Suite-level contract; zero = inherit from module */
} WordTestSuite;
/** @name Core Test Execution Functions
* @{
*/
/*
* @brief Executes a single test case
* @param vm Pointer to the VM instance
* @param word_name Name of the word being tested
* @param test Test case to execute
* @return Test execution result
*/
TestResult run_single_test(VM *vm, const char *word_name, const TestCase *test);
/*
* @brief Runs all tests in a test suite
* @param vm Pointer to the VM instance
* @param suite Test suite to execute
*/
void run_test_suite(VM *vm, const WordTestSuite *suite);
/**
* @brief Runs all tests in a suite with an inherited module-level contract.
*
* Contract resolution order (first non-NONE wins):
* test->contract > suite->suite_contract > module_contract
*
* When CONTRACTS_ENABLED=1, each passing TEST_NORMAL test is re-run under
* physics perturbation to witness A4' (word_physics_transparent).
*
* @param vm Pointer to the VM instance
* @param suite Test suite to execute
* @param module_contract Default contract for tests that don't specify one
*/
void run_test_suite_m(VM *vm, const WordTestSuite *suite, WordContract module_contract);
/*
* @brief Prints test results_run_01_2025_12_08 summary for a module
* @param module_name Name of the module
* @param pass Number of passed tests
* @param fail Number of failed tests
* @param skip Number of skipped tests
* @param error Number of errors
*/
void print_module_summary(const char *module_name, int pass, int fail, int skip, int error);
/** @} */
/** @name Test Assertion Functions
* @{
*/
/*
* @brief Verifies the VM stack depth
* @param vm Pointer to the VM instance
* @param expected_depth Expected stack depth
* @return 1 if assertion passes, 0 otherwise
*/
int assert_stack_depth(VM *vm, int expected_depth);
/*
* @brief Verifies the value at the top of the stack
* @param vm Pointer to the VM instance
* @param expected_value Expected value
* @return 1 if assertion passes, 0 otherwise
*/
int assert_stack_top(VM *vm, int expected_value);
/*
* @brief Verifies VM error state
* @param vm Pointer to the VM instance
* @param should_have_error Expected error state
* @return 1 if assertion passes, 0 otherwise
*/
int assert_vm_error(VM *vm, int should_have_error);
/** @} */
/** @name VM State Management Functions
* @{
*/
/*
* @brief Saves current VM state
* @param vm Pointer to the VM instance
* @param dsp Pointer to store data stack pointer
* @param rsp Pointer to store return stack pointer
* @param error Pointer to store error state
* @param mode Pointer to store VM mode
*/
void save_vm_state(VM *vm, int *dsp, int *rsp, int *error, vm_mode_t *mode);
/*
* @brief Restores VM state from saved values
* @param vm Pointer to the VM instance
* @param dsp Data stack pointer to restore
* @param rsp Return stack pointer to restore
* @param error Error state to restore
* @param mode VM mode to restore
*/
void restore_vm_state(VM *vm, int dsp, int rsp, int error, vm_mode_t mode);
/*
* @brief Saves dictionary state before test execution
* @param vm Pointer to VM instance
* @param latest Pointer to store latest dictionary entry
* @param here Pointer to store HERE pointer
*/
void save_dict_state(VM * vm, DictEntry * *latest, size_t * here);
/*
* @brief Restores dictionary state after test execution
* @param vm Pointer to VM instance
* @param latest Latest dictionary entry to restore
* @param here HERE pointer to restore
* @details Removes all words added during test by restoring dictionary pointers
*/
void restore_dict_state(VM *vm, DictEntry *latest, size_t here);
/** @} */
#endif /* TEST_COMMON_H */
+121
View File
@@ -0,0 +1,121 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
Licensed under the Starship License 1.0 (SL-1.0)
*/
#ifndef TEST_CONTRACTS_H
#define TEST_CONTRACTS_H
#include "vm.h" /* resolved via -Iinclude */
#include <stdint.h>
#include <stddef.h>
/* ============================================================================
* Word Contract System — Runtime witnesses for Isabelle/HOL axioms
*
* Two axioms require runtime verification:
* A1 heartbeat_exec_neutral — vm_tick() must not write exec-equiv fields
* A4' word_physics_transparent — word results must be independent of physics state
*
* Contracts attach at three levels; resolution order (first non-NONE wins):
* individual TestCase > WordTestSuite.suite_contract > module_contract arg
*
* Contract checks run unconditionally as part of POST.
* ============================================================================ */
typedef uint32_t contract_flags_t;
#define CONTRACT_NONE 0u
/* A4': re-run word with perturbed physics; assert identical exec result */
#define CONTRACT_PHYSICS_TRANSPARENT (1u << 0)
/* A1: call vm_tick() and assert exec-equiv fields are unchanged */
#define CONTRACT_HEARTBEAT_SAFE (1u << 1)
/* Number of physics perturbation rounds per test case (0 = use default) */
#define CONTRACT_DEFAULT_FUZZ_ROUNDS 4
typedef struct {
contract_flags_t flags;
int fuzz_rounds; /* 0 = use CONTRACT_DEFAULT_FUZZ_ROUNDS */
} WordContract;
/* Exec-domain snapshot — exactly the four fields in exec_equiv */
typedef struct {
cell_t stack[STACK_SIZE];
int dsp;
int rsp;
int error;
} ExecResult;
/* Physics field snapshot for save/restore around A4' checks */
typedef struct {
uint64_t decay_slope_q48;
uint32_t eff_window_size;
uint64_t tick_target_ns;
cell_t heat_25th;
cell_t heat_50th;
cell_t heat_75th;
uint64_t prefetch_attempts;
uint64_t total_executions;
} PhysicsSnapshot;
/* Global counter incremented on each contract violation (CONTRACTS_ENABLED only) */
extern int global_contract_violations;
/*
* Accessor functions for global_contract_violations.
* Use these from other TUs — direct extern access generates GOTPCRELX which
* the PE linker does not relax, causing double-dereference bugs in -fPIC PE builds.
*/
int contract_get_violation_count(void);
void contract_reset_violation_count(void);
/*
* capture_exec_result — snapshot the four exec-equiv fields from vm into r.
*/
void capture_exec_result(VM *vm, ExecResult *r);
/*
* exec_results_equal — 1 if a and b agree on dsp, rsp, error, and live stack.
*/
int exec_results_equal(const ExecResult *a, const ExecResult *b);
/*
* save_physics / restore_physics — snapshot and restore key physics scalars.
*/
void save_physics(VM *vm, PhysicsSnapshot *s);
void restore_physics(VM *vm, const PhysicsSnapshot *s);
/*
* perturb_physics — replace key physics scalars with fresh random values.
* round is used to vary the RNG seed across multiple rounds.
*/
void perturb_physics(VM *vm, int round);
/*
* check_physics_transparent — A4' contract check for a single test case.
*
* Runs input with the current physics state (baseline), then runs it again
* fuzz_rounds times with physics perturbed, asserting identical ExecResult
* each time. Returns 1 if all rounds match, 0 on first violation (which is
* also logged as an error). Skips error-expected and unimplemented tests.
*
* NOTE: input must not define dictionary words; the dict is not snapshotted
* inside this function. Callers that cover defining-words tests should
* save/restore dict state around this call.
*/
int check_physics_transparent(VM *vm, const char *word_name,
const char *input,
int should_error, int implemented,
int fuzz_rounds);
/*
* assert_heartbeat_safe — A1 contract check.
*
* Snapshots exec-equiv fields, calls vm_tick(), then asserts all four fields
* are unchanged. Returns 1 if the assertion holds, 0 on violation.
*/
int assert_heartbeat_safe(VM *vm);
#endif /* TEST_CONTRACTS_H */
+181
View File
@@ -0,0 +1,181 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef TEST_RUNNER_H
#define TEST_RUNNER_H
#include "../../../include/vm.h"
#include <stdint.h>
/**
* @brief Structure holding test execution statistics
*/
typedef struct {
int total_tests; /**< Total number of tests executed */
int total_pass; /**< Number of passed tests */
int total_fail; /**< Number of failed tests */
int total_skip; /**< Number of skipped tests */
int total_error; /**< Number of tests that resulted in errors */
} TestStats;
/**
* @brief Structure representing a test module
*/
typedef struct {
const char *module_name; /**< Name of the test module */
void *reserved; /**< Reserved for future use */
int reserved_int; /**< Reserved integer value */
void (*run_module_tests)(VM *vm); /**< Function pointer to run module tests */
} TestModule;
/* Global test statistics - extern declaration */
extern TestStats global_test_stats;
/*
* Same-TU accumulator — use instead of direct struct access from other TUs.
* Direct extern struct access generates R_X86_64_REX_GOTPCRELX which the
* PE linker does not relax, causing double-dereference in -fPIC PE builds.
*/
void test_stats_accumulate(int tests, int pass, int fail, int skip, int error);
/*
* @brief Run tests for stack manipulation words
* @param vm Pointer to the VM instance
*/
void run_stack_words_tests(VM * vm);
void run_return_stack_words_tests(VM * vm);
void run_memory_words_tests(VM * vm);
void run_arithmetic_words_tests(VM * vm);
void run_logical_words_tests(VM * vm);
void run_mixed_arithmetic_words_tests(VM * vm);
void run_double_words_tests(VM * vm);
void run_dictionary_words_tests(VM * vm);
void run_io_words_tests(VM * vm);
void run_format_words_tests(VM * vm);
void run_string_words_tests(VM * vm);
void run_dictionary_manipulation_words_tests(VM * vm);
void run_vocabulary_words_tests(VM * vm);
void run_block_words_tests(VM * vm);
void run_system_words_tests(VM * vm);
void run_defining_words_tests(VM * vm);
void run_control_words_tests(VM * vm);
void run_starforth_words_tests(VM * vm);
void run_acl_words_tests(VM * vm);
/*
* @brief Run tests for Q48.16 fixed-point words (Module 25)
* @param vm Pointer to the VM instance
*/
void run_q48_words_tests(VM * vm);
/*
* @brief Run tests for the proof-covered subset of inference-engine words (Module 26)
* @param vm Pointer to the VM instance
*/
void run_inference_words_tests(VM * vm);
/*
* @brief Run tests for the proof-covered subset of physics-freeze words (Module 27)
* @param vm Pointer to the VM instance
*/
void run_physics_freeze_words_tests(VM * vm);
/*
* @brief Run tests for Mama FORTH vocabulary (capsule system M7.1)
* @param vm Pointer to the VM instance
*
* In hosted builds, all tests are skipped (Mama words are kernel-only).
* In kernel builds, tests validate capsule enumeration and VM birth.
*/
void run_mama_forth_words_tests(VM * vm);
/*
* @brief Enable benchmark mode for performance testing
* @param iterations Number of iterations for benchmark tests
*/
void enable_benchmark_mode(int iterations);
/*
* @brief Execute stress tests
* @param vm Pointer to the VM instance
*/
void run_stress_tests(VM * vm);
/*
* @brief Execute integration tests
* @param vm Pointer to the VM instance
*/
void run_integration_tests(VM * vm);
/*
* @brief Run all available tests
* @param vm Pointer to the VM instance
*/
void run_all_tests(VM * vm);
/*
* @brief Run tests for a specific module
* @param vm Pointer to the VM instance
* @param module_name Name of the module to test
*/
void run_module_tests(VM *vm, const char *module_name);
/*
* @brief Run tests for a specific word
* @param vm Pointer to the VM instance
* @param word_name Name of the word to test
*/
void run_word_tests(VM *vm, const char *word_name);
/**
* @brief Run ultra-comprehensive break-me diagnostic suite
* @param vm Pointer to the VM instance
*
* Executes the most exhaustive test battery ever designed for StarForth,
* generates a detailed markdown report in docs/BREAK_ME_REPORT.md,
* and includes a surprise easter egg at the end.
*/
void run_break_me_tests(VM * vm);
#endif /* TEST_RUNNER_H */
+26
View File
@@ -0,0 +1,26 @@
# src/test_runner/modules/
Per-category test files, one per FORTH-79 word category plus
integration/stress/adversarial suites, run in this order by
`test_runner.c` (POST order).
**Word-category tests** (validate each `src/word_source/*.c` file):
`arithmetic_words_test.c`, `mixed_arithmetic_words_test.c`,
`stack_words_test.c`, `control_words_test.c`, `defining_words_tests.c`,
`memory_words_test.c`, `return_stack_words_test.c`, `double_words_test.c`,
`logical_words_test.c`, `io_words_test.c`, `string_words_test.c`,
`block_words_test.c`, `format_words_test.c`, `system_words_test.c`,
`dictionary_words_test.c`, `dictionary_manipulation_words_test.c`,
`vocabulary_words_test.c`, `starforth_words_test.c`,
`mama_forth_words_test.c`, `acl_words_test.c`.
**Cross-cutting tests**:
- `integration_tests.c` — multi-word interaction tests.
- `stress_tests.c` — load/volume tests.
- `break_me_tests.c` — adversarial/fuzzing tests.
Note: not every `src/word_source/*.c` file has a dedicated test module
here (e.g. `defer_words.c`, `log_words.c`, `inference_words.c`,
`lifecycle_words_hosted.c`, `editor_words.c` are exercised indirectly via
integration tests or the `tools/hermes_*.sh` smoke tests rather than a
standalone `*_test.c` module).
+628
View File
@@ -0,0 +1,628 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* acl_words_test.c — POST tests for the word-level ACL system (Module 24)
*
* Tests directly manipulate DictEntry ACL fields in C and then verify
* behaviour through the interpreter or via direct field inspection.
* This avoids depending on ACL.4th being loaded during POST.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
#include "../../../include/vm.h"
#include "../../word_source/include/acl_words.h"
#include <stdio.h>
/* ------------------------------------------------------------------ */
static int tests_passed = 0;
static int tests_failed = 0;
#define ACL_ASSERT(cond, msg) \
do { \
if (cond) { \
tests_passed++; \
log_message(LOG_DEBUG, " PASS: %s", msg); \
} else { \
tests_failed++; \
log_message(LOG_ERROR, " FAIL: %s", msg); \
} \
} while (0)
/* ------------------------------------------------------------------ */
/**
* @brief Test 1 — ACL-PIN is a one-way ratchet.
*
* Creates a temporary word and pins it via @c ACL-PIN. Verifies that
* subsequent calls to @c ACL-MODE!, @c ACL-TTL!, and @c ACL-ALLOW! are
* all no-ops while the word is pinned. The pin flag itself must remain 1.
*
* Expected assertions (4):
* - @c acl_pinned == 1 after ACL-PIN
* - @c acl_mode unchanged after ACL-MODE! on pinned entry
* - @c acl_ttl unchanged after ACL-TTL! on pinned entry
* - @c acl_allow unchanged after ACL-ALLOW! on pinned entry
*
* @param vm VM instance (shared state; caller restores after this function).
*/
static void test_acl_pin_immutable(VM *vm)
{
DictEntry *e = vm_create_word(vm, "__acl_pin_test__", 16, NULL);
if (!e) { tests_failed++; return; }
e->acl_mode = ACL_MODE_TTL;
e->acl_pinned = 0;
/* Pin it */
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-PIN");
vm->error = 0;
ACL_ASSERT(e->acl_pinned == 1, "ACL-PIN sets acl_pinned=1");
/* Attempt to change mode via ACL-MODE! (C primitive) — should be no-op when pinned */
vm_push(vm, (cell_t)ACL_MODE_STRICT);
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-MODE!");
vm->error = 0;
ACL_ASSERT(e->acl_mode == ACL_MODE_TTL, "ACL-STRICT is no-op when pinned");
/* Attempt to change TTL — should be ignored */
uint32_t old_ttl = e->acl_ttl;
vm_push(vm, (cell_t)9999);
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-TTL!");
vm->error = 0;
ACL_ASSERT(e->acl_ttl == old_ttl, "ACL-TTL! is no-op when pinned");
/* Attempt to change allow — should be ignored */
e->acl_allow = 1;
vm_push(vm, (cell_t)0);
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-ALLOW!");
vm->error = 0;
ACL_ASSERT(e->acl_allow == 1, "ACL-ALLOW! is no-op when pinned");
}
/**
* @brief Test 2 — ACL-INHERIT copies mode, clears pin, and resets TTL/allow.
*
* Constructs a source entry with STRICT mode, pinned, TTL=5000, allow=0,
* and a destination entry with a different mode and also pinned. Calls
* @c acl_inherit_entry() directly and checks the resulting destination
* state. Verifies the Isabelle/HOL lemma @c ACL_Inherit_Clears_Pin.
*
* Expected assertions (4):
* - @c dst->acl_mode == ACL_MODE_STRICT (mode copied from src)
* - @c dst->acl_pinned == 0 (pin cleared regardless of dst's original pin)
* - @c dst->acl_ttl == 0 (TTL reset to 0)
* - @c dst->acl_allow == 1 (allow reset to 1)
*
* @param vm VM instance (used for word creation; not modified beyond that).
*/
static void test_acl_inherit(VM *vm)
{
DictEntry *src = vm_create_word(vm, "__acl_src__", 11, NULL);
DictEntry *dst = vm_create_word(vm, "__acl_dst__", 11, NULL);
if (!src || !dst) { tests_failed++; return; }
src->acl_mode = ACL_MODE_STRICT;
src->acl_pinned = 1;
src->acl_ttl = 5000;
src->acl_allow = 0;
dst->acl_pinned = 1; /* even a pinned dst should be cleared */
dst->acl_mode = ACL_MODE_TTL;
acl_inherit_entry(src, dst);
ACL_ASSERT(dst->acl_mode == ACL_MODE_STRICT, "ACL-INHERIT copies acl_mode");
ACL_ASSERT(dst->acl_pinned == 0, "ACL-INHERIT clears acl_pinned");
ACL_ASSERT(dst->acl_ttl == 0, "ACL-INHERIT resets acl_ttl to 0");
ACL_ASSERT(dst->acl_allow == 1, "ACL-INHERIT resets acl_allow to 1");
}
/**
* @brief Test 3 — emergency_console flag bypasses a denied word.
*
* Defines a FORTH word that pushes 42, then sets its @c acl_allow to 0
* to simulate a policy denial. Executes the word with @c emergency_console=1
* and verifies that the word body ran (stack depth increased by 1).
*
* The @c emergency_console bypass is required by the bare-metal fault
* handler so that critical recovery words can always execute.
*
* Expected assertions (1):
* - @c vm->dsp == saved_dsp + 1 (word ran despite deny)
*
* @param vm VM instance.
*/
static void test_emergency_console_bypass(VM *vm)
{
/* Define a simple FORTH word that pushes a marker */
vm_interpret(vm, ": __acl_ec_test__ 42 ;");
vm->error = 0;
DictEntry *e = vm_find_word(vm, "__acl_ec_test__", 15);
if (!e) { tests_failed++; return; }
/* Deny it */
e->acl_allow = 0;
e->acl_ttl = 0;
e->acl_pinned = 0;
/* With emergency_console=1: should execute despite deny */
int saved_dsp = vm->dsp;
vm->emergency_console = 1;
vm_interpret(vm, "__acl_ec_test__");
vm->emergency_console = 0;
vm->error = 0;
ACL_ASSERT(vm->dsp == saved_dsp + 1, "emergency_console bypasses ACL deny");
if (vm->dsp > saved_dsp) vm->dsp--; /* pop the 42 */
/* Restore allow so subsequent tests don't trip over it */
e->acl_allow = 1;
}
/**
* @brief Test 4 — ACL denial aborts execution without running the word body.
*
* Defines a word that pushes a sentinel (99), then denies it with
* @c acl_allow=0 and @c acl_ttl=1 (hot-path decrement triggers the deny
* check without invoking @c acl_recheck()). Verifies that executing the
* word sets either @c vm->abort_requested, @c vm->error, or leaves the
* stack depth unchanged — and that the sentinel was never pushed.
*
* @c acl_ttl=1 is used rather than 0 to stay on the hot path (TTL
* decrements to 0 → check; avoids the fail-open cold-path recheck).
*
* Expected assertions (2):
* - Execution was aborted (abort_requested || error || stack unchanged)
* - Stack depth is unchanged (sentinel was not pushed)
*
* @param vm VM instance.
*/
static void test_acl_deny_aborts(VM *vm)
{
/* Define a word that pushes a sentinel value */
vm_interpret(vm, ": __acl_deny_test__ 99 ;");
vm->error = 0;
DictEntry *e = vm_find_word(vm, "__acl_deny_test__", 17);
if (!e) { tests_failed++; return; }
e->acl_allow = 0;
e->acl_ttl = 1; /* hot path: decrement then check; avoids fail-open recheck */
e->acl_pinned = 0;
int saved_dsp = vm->dsp;
vm->emergency_console = 0;
vm_interpret(vm, "__acl_deny_test__");
int was_aborted = (vm->abort_requested || vm->error || vm->dsp == saved_dsp);
vm->abort_requested = 0;
vm->error = 0;
ACL_ASSERT(was_aborted, "denied word triggers abort");
ACL_ASSERT(vm->dsp == saved_dsp, "denied word: stack unchanged");
e->acl_allow = 1;
}
/**
* @brief Test 5 — ACL TTL hot-path decrements without invoking recheck.
*
* Defines a word with @c acl_ttl=10 and @c acl_allow=1, then executes
* it three times via the outer interpreter. Each outer-interpreter
* execution decrements @c acl_ttl by 1, so the expected final value is 7.
*
* This confirms the hot-path optimisation: the interpreter only calls
* @c acl_recheck() when @c acl_ttl reaches 0, not on every invocation.
*
* Expected assertions (1):
* - @c e->acl_ttl == 7 after three executions (10 → 9 → 8 → 7)
*
* @param vm VM instance.
*/
static void test_acl_ttl_hot_path(VM *vm)
{
vm_interpret(vm, ": __acl_ttl_test__ 1 DROP ;");
vm->error = 0;
DictEntry *e = vm_find_word(vm, "__acl_ttl_test__", 16);
if (!e) { tests_failed++; return; }
e->acl_allow = 1;
e->acl_ttl = 10;
e->acl_pinned = 0;
e->acl_mode = ACL_MODE_TTL;
/* Execute the word 3 times via the outer interpreter */
vm->emergency_console = 0;
vm_interpret(vm, "__acl_ttl_test__");
vm->error = 0;
vm_interpret(vm, "__acl_ttl_test__");
vm->error = 0;
vm_interpret(vm, "__acl_ttl_test__");
vm->error = 0;
ACL_ASSERT(e->acl_ttl == 7, "TTL hot path: TTL decrements from 10 to 7 after 3 execs");
}
/**
* @brief Test 6 — STRICT mode keeps TTL at 0 after every recheck.
*
* In @c ACL_MODE_STRICT, @c ACL-RECHECK must reset @c acl_ttl to 0 after
* each allow decision so the recheck fires on every subsequent execution.
*
* If @c ACL.4th is not loaded, @c ACL-RECHECK is absent and @c acl_recheck()
* fails open (@c acl_ttl = @c ACL_TTL_OPEN). In that case the test is
* conditionally skipped (counted as a pass) since the ACL system is not
* yet active.
*
* Expected assertions (1, or skip counted as 1 pass):
* - @c e->acl_ttl == 0 after one execution in STRICT mode with ACL loaded.
*
* @param vm VM instance.
*/
static void test_acl_strict_mode(VM *vm)
{
vm_interpret(vm, ": __acl_strict_test__ DROP ;");
vm->error = 0;
DictEntry *e = vm_find_word(vm, "__acl_strict_test__", 19);
if (!e) { tests_failed++; return; }
e->acl_mode = ACL_MODE_STRICT;
e->acl_allow = 1;
e->acl_ttl = 0;
e->acl_pinned = 0;
/* ACL-RECHECK for STRICT mode must reset TTL to 0 after allowing */
vm_push(vm, (cell_t)(uintptr_t)e);
DictEntry *recheck = vm_find_word(vm, "ACL-RECHECK", 11);
if (recheck) {
/* ACL-RECHECK is not yet in the dict (ACL.4th not loaded) — skip FORTH call */
/* Test via acl_recheck() directly: in STRICT mode, TTL stays 0 */
/* We can't call acl_recheck() directly (static), but we can test
that the vm_interpret path triggers the recheck path correctly.
After execution with TTL=0 and allow=1, TTL should still be 0
in STRICT mode (recheck was called and set TTL=0 again). */
vm->dsp--; /* clean up the push above */
vm_interpret(vm, "0 __acl_strict_test__");
vm->error = 0;
ACL_ASSERT(e->acl_ttl == 0, "STRICT mode: TTL remains 0 after recheck");
} else {
/* ACL.4th not loaded yet — fallback sets TTL=OPEN; note but don't fail */
vm->dsp--;
log_message(LOG_INFO, " SKIP: STRICT mode test (ACL.4th not loaded)");
tests_passed++; /* conditional skip counts as pass in POST */
}
}
/**
* @brief Test 7 — Adaptive TTL scales with execution heat via ACL-TTL-COMPUTE.
*
* Sets @c execution_heat=2560 on a temporary entry and invokes
* @c ACL-TTL-COMPUTE if the word is present (i.e., @c ACL.4th is loaded).
* The expected formula is @c ttl = heat/4 + 256 = 640 + 256 = 896.
*
* If @c ACL-TTL-COMPUTE is not present (ACL.4th not yet loaded), the test
* is conditionally skipped and counted as a pass.
*
* Expected assertions (1, or skip):
* - @c ACL-TTL-COMPUTE returns 896 for heat=2560.
*
* @param vm VM instance.
*/
static void test_acl_adaptive_ttl(VM *vm)
{
DictEntry *e = vm_create_word(vm, "__acl_heat_test__", 17, NULL);
if (!e) { tests_failed++; return; }
/* Set heat and verify TTL-COMPUTE via FORTH (if ACL.4th loaded) */
e->execution_heat = 2560;
e->acl_allow = 1;
e->acl_pinned = 0;
DictEntry *ttl_compute = vm_find_word(vm, "ACL-TTL-COMPUTE", 15);
if (ttl_compute) {
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-TTL-COMPUTE");
vm->error = 0;
if (vm->dsp >= 0) {
cell_t ttl = vm_pop(vm);
/* heat=2560, ttl = 2560/4 + 256 = 640+256 = 896 */
ACL_ASSERT(ttl == 896, "ACL-TTL-COMPUTE: heat=2560 → ttl=896");
} else {
tests_failed++;
log_message(LOG_ERROR, " FAIL: ACL-TTL-COMPUTE left empty stack");
}
} else {
log_message(LOG_INFO, " SKIP: ACL-TTL-COMPUTE (ACL.4th not loaded)");
tests_passed++;
}
vm->error = 0;
}
/**
* @brief Test 8 — A pinned word cannot be shadowed by redefinition.
*
* Creates a word, pins it, then attempts to create another word with
* the same name via @c vm_create_word(). Verifies that the second create
* returns NULL and sets @c vm->error, and that the original pinned entry
* is still the one returned by @c vm_find_word().
*
* This enforces the Isabelle/HOL lemma @c ACL_Pin_Monotone: pin is
* permanent and cannot be overridden even by a new definition.
*
* Expected assertions (3):
* - @c vm_create_word() returns NULL for the shadowed name
* - @c vm->error is non-zero after the blocked create
* - @c vm_find_word() still returns the original pinned entry
*
* @param vm VM instance.
*/
static void test_acl_pin_blocks_shadow(VM *vm)
{
DictEntry *orig = vm_create_word(vm, "__acl_shadow_target__", 21, NULL);
if (!orig) { tests_failed++; return; }
orig->acl_pinned = 1;
DictEntry *shadow = vm_create_word(vm, "__acl_shadow_target__", 21, NULL);
ACL_ASSERT(shadow == NULL, "vm_create_word returns NULL for pinned name");
ACL_ASSERT(vm->error != 0, "vm->error set when shadow blocked");
vm->error = 0;
DictEntry *found = vm_find_word(vm, "__acl_shadow_target__", 21);
ACL_ASSERT(found == orig, "original pinned entry still found after blocked shadow");
}
/**
* @brief Test 9 — ACL-MODE@/ACL-PINNED?/ACL-TTL@/ACL-ALLOW@/ACL-HEAT@/
* ACL-WORD-ID read a word's ACL fields via the interpreter.
*
* Proof-covered field accessors (`proof/StarForth_ACL_*.thy`) that had no
* interpreter-level POST coverage before this test — every other ACL test
* in this file exercises the write accessors (`ACL-MODE!`/`ACL-TTL!`/
* `ACL-ALLOW!`) or calls the underlying C functions directly, but never
* pushed an xt and interpreted these six read words.
*
* Sets each field to a distinctive non-default value directly on a fresh
* test word, then pushes its xt and interprets each accessor in turn,
* checking the value returned on the stack.
*
* Expected assertions (6):
* - ACL-MODE@ returns @c ACL_MODE_STRICT
* - ACL-PINNED? returns -1 (true) for a pinned entry
* - ACL-TTL@ returns 4242
* - ACL-ALLOW@ returns 0 (false)
* - ACL-HEAT@ returns the entry's @c execution_heat
* - ACL-WORD-ID returns the entry's @c word_id
*
* @param vm VM instance.
*/
static void test_acl_read_accessors(VM *vm)
{
DictEntry *e = vm_create_word(vm, "__acl_read_test__", 18, NULL);
if (!e) { tests_failed++; return; }
e->acl_mode = ACL_MODE_STRICT;
e->acl_pinned = 1;
e->acl_ttl = 4242;
e->acl_allow = 0;
e->execution_heat = 777;
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-MODE@");
vm->error = 0;
ACL_ASSERT(vm_pop(vm) == (cell_t)ACL_MODE_STRICT, "ACL-MODE@ reads acl_mode");
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-PINNED?");
vm->error = 0;
ACL_ASSERT(vm_pop(vm) == -1, "ACL-PINNED? reads acl_pinned as true");
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-TTL@");
vm->error = 0;
ACL_ASSERT(vm_pop(vm) == 4242, "ACL-TTL@ reads acl_ttl");
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-ALLOW@");
vm->error = 0;
ACL_ASSERT(vm_pop(vm) == 0, "ACL-ALLOW@ reads acl_allow as false");
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-HEAT@");
vm->error = 0;
ACL_ASSERT(vm_pop(vm) == (cell_t)e->execution_heat, "ACL-HEAT@ reads execution_heat");
vm_push(vm, (cell_t)(uintptr_t)e);
vm_interpret(vm, "ACL-WORD-ID");
vm->error = 0;
ACL_ASSERT(vm_pop(vm) == (cell_t)e->word_id, "ACL-WORD-ID reads word_id");
}
/**
* @brief Test 10 — ACL-INHERIT, as an interpreted FORTH word (src dst -- ).
*
* `test_acl_inherit` above already proves the underlying
* @c acl_inherit_entry() C function is correct (Isabelle lemma
* @c ACL_Inherit_Clears_Pin) by calling it directly. This test instead
* exercises the FORTH-level dispatch path itself — pushing two xts and
* interpreting "ACL-INHERIT" — which nothing in this file previously did,
* so the word's own stack-popping/argument-order code was untested.
*
* Expected assertions (2):
* - @c dst->acl_mode == ACL_MODE_STRICT after interpreting ACL-INHERIT
* - @c dst->acl_pinned == 0 after interpreting ACL-INHERIT
*
* @param vm VM instance.
*/
static void test_acl_inherit_word_dispatch(VM *vm)
{
DictEntry *src = vm_create_word(vm, "__acl_inh_src__", 15, NULL);
DictEntry *dst = vm_create_word(vm, "__acl_inh_dst__", 15, NULL);
if (!src || !dst) { tests_failed++; return; }
src->acl_mode = ACL_MODE_STRICT;
src->acl_pinned = 0;
dst->acl_pinned = 1;
dst->acl_mode = ACL_MODE_TTL;
vm_push(vm, (cell_t)(uintptr_t)src);
vm_push(vm, (cell_t)(uintptr_t)dst);
vm_interpret(vm, "ACL-INHERIT");
vm->error = 0;
ACL_ASSERT(dst->acl_mode == ACL_MODE_STRICT, "ACL-INHERIT (word) copies acl_mode");
ACL_ASSERT(dst->acl_pinned == 0, "ACL-INHERIT (word) clears acl_pinned");
}
/**
* @brief Test 11 — ACL-INIT-PRIMITIVES resets unpinned words, skips pinned ones.
*
* Creates two fresh words with non-default ACL state: one left unpinned,
* one pinned. Interprets "ACL-INIT-PRIMITIVES" (which walks the entire
* dictionary) and verifies the unpinned word was reset to the permissive
* defaults (@c acl_ttl=0, @c acl_allow=1, @c acl_mode=ACL_MODE_TTL) while
* the pinned word's deliberately-different state was left untouched.
*
* Expected assertions (4):
* - unpinned word's @c acl_ttl reset to 0
* - unpinned word's @c acl_allow reset to 1
* - unpinned word's @c acl_mode reset to ACL_MODE_TTL
* - pinned word's @c acl_ttl is NOT reset (still its original value)
*
* @param vm VM instance.
*/
static void test_acl_init_primitives(VM *vm)
{
DictEntry *unpinned = vm_create_word(vm, "__acl_initp_free__", 19, NULL);
DictEntry *pinned = vm_create_word(vm, "__acl_initp_pin__", 18, NULL);
if (!unpinned || !pinned) { tests_failed++; return; }
unpinned->acl_ttl = 555;
unpinned->acl_allow = 0;
unpinned->acl_mode = ACL_MODE_STRICT;
unpinned->acl_pinned = 0;
pinned->acl_ttl = 555;
pinned->acl_allow = 0;
pinned->acl_mode = ACL_MODE_STRICT;
pinned->acl_pinned = 1;
vm_interpret(vm, "ACL-INIT-PRIMITIVES");
vm->error = 0;
ACL_ASSERT(unpinned->acl_ttl == 0, "ACL-INIT-PRIMITIVES resets unpinned acl_ttl");
ACL_ASSERT(unpinned->acl_allow == 1, "ACL-INIT-PRIMITIVES resets unpinned acl_allow");
ACL_ASSERT(unpinned->acl_mode == ACL_MODE_TTL, "ACL-INIT-PRIMITIVES resets unpinned acl_mode");
ACL_ASSERT(pinned->acl_ttl == 555, "ACL-INIT-PRIMITIVES leaves pinned entry untouched");
}
/* ------------------------------------------------------------------ */
/**
* @brief Module 24 entry point — run all word-level ACL POST tests.
*
* Saves the current VM state (dsp, rsp, error, mode, emergency_console)
* before each sub-test and restores it between tests so failures do not
* cascade. Runs eleven tests covering:
*
* 1. ACL-PIN one-way ratchet (4 assertions)
* 2. ACL-INHERIT semantics (4 assertions)
* 3. emergency_console bypass (1 assertion)
* 4. ACL denial aborts word execution (2 assertions)
* 5. TTL hot-path decrement (1 assertion)
* 6. STRICT mode — TTL stays 0 (1 assertion or conditional skip)
* 7. Adaptive TTL via ACL-TTL-COMPUTE (1 assertion or conditional skip)
* 8. Pin blocks shadowing (3 assertions)
* 9. Read accessors — ACL-MODE@/PINNED?/TTL@/ALLOW@/HEAT@/WORD-ID (6 assertions)
* 10. ACL-INHERIT as an interpreted word, not just its C twin (2 assertions)
* 11. ACL-INIT-PRIMITIVES resets unpinned, skips pinned (4 assertions)
*
* Tests that depend on @c ACL.4th being loaded are conditionally skipped
* during bare POST (before @c ACL.4th runs) and counted as passes so the
* POST score is not penalised by load-order.
*
* @param vm VM instance; must be initialised and have the standard
* word vocabulary available.
*/
void run_acl_words_tests(VM *vm)
{
log_message(LOG_INFO, "Running ACL Words Tests (Module 24)...");
tests_passed = 0;
tests_failed = 0;
int saved_dsp, saved_rsp, saved_error;
vm_mode_t saved_mode;
save_vm_state(vm, &saved_dsp, &saved_rsp, &saved_error, &saved_mode);
uint8_t saved_ec = vm->emergency_console;
test_acl_pin_immutable(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_inherit(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_emergency_console_bypass(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_deny_aborts(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_ttl_hot_path(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_strict_mode(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_adaptive_ttl(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_pin_blocks_shadow(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_read_accessors(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_inherit_word_dispatch(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
test_acl_init_primitives(vm);
restore_vm_state(vm, saved_dsp, saved_rsp, 0, saved_mode);
vm->emergency_console = saved_ec;
print_module_summary("ACL Words", tests_passed, tests_failed, 0, 0);
}
@@ -0,0 +1,285 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Arithmetic Words Test Suites - Module 5 */
static WordTestSuite arithmetic_word_suites[] = {
{
"+", {
{"basic", "5 7 + . CR", "Should print: 12", TEST_NORMAL, 0, 1, {0}},
{"zero_add", "42 0 + . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-5 3 + . CR", "Should print: -2", TEST_NORMAL, 0, 1, {0}},
{"both_negative", "-5 -3 + . CR", "Should print: -8", TEST_NORMAL, 0, 1, {0}},
{"overflow", "2147483647 1 + . CR", "Should overflow", TEST_EDGE_CASE, 0, 1, {0}},
{"underflow", "-2147483648 -1 + . CR", "Should underflow", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "+", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 +", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
8, {0}
},
{
"-", {
{"basic", "10 3 - . CR", "Should print: 7", TEST_NORMAL, 0, 1, {0}},
{"zero_sub", "42 0 - . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"from_zero", "0 5 - . CR", "Should print: -5", TEST_NORMAL, 0, 1, {0}},
{"negative", "5 -3 - . CR", "Should print: 8", TEST_NORMAL, 0, 1, {0}},
{"same_values", "42 42 - . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"overflow", "-2147483648 1 - . CR", "Should underflow", TEST_EDGE_CASE, 0, 1, {0}},
{"underflow", "2147483647 -1 - . CR", "Should overflow", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "-", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 -", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
9, {0}
},
{
"*", {
{"basic", "6 7 * . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "42 0 * . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"by_one", "42 1 * . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"by_negative", "6 -7 * . CR", "Should print: -42", TEST_NORMAL, 0, 1, {0}},
{"negative_negative", "-6 -7 * . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"large_numbers", "32767 2 * . CR", "Should print: 65534", TEST_EDGE_CASE, 0, 1, {0}},
{"overflow", "65536 65536 * . CR", "May overflow", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "*", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 *", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
9, {0}
},
{
"/", {
{"basic", "15 3 / . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"by_one", "42 1 / . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative_dividend", "-15 3 / . CR", "Should print: -5", TEST_NORMAL, 0, 1, {0}},
{"negative_divisor", "15 -3 / . CR", "Should print: -5", TEST_NORMAL, 0, 1, {0}},
{"both_negative", "-15 -3 / . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"truncation", "7 3 / . CR", "Should print: 2 (truncated)", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "42 0 /", "Should cause division by zero", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "/", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 /", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
9, {0}
},
{
"MOD", {
{"basic", "17 5 MOD . CR", "Should print: 2", TEST_NORMAL, 0, 1, {0}},
{"exact_division", "15 3 MOD . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"by_one", "42 1 MOD . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative_dividend", "-17 5 MOD . CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"negative_divisor", "17 -5 MOD . CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"small_by_large", "3 7 MOD . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "42 0 MOD", "Should cause division by zero", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "MOD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 MOD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
9, {0}
},
{
"/MOD", {
{"basic", "17 5 /MOD . . CR", "Should print: 3 2", TEST_NORMAL, 0, 1, {0}},
{"exact", "15 3 /MOD . . CR", "Should print: 5 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-17 5 /MOD . . CR","Should handle negative dividend", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "42 0 /MOD", "Should cause division by zero", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "/MOD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 /MOD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"ABS", {
{"positive", "42 ABS . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 ABS . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 ABS . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"min_int", "-2147483648 ABS . CR", "Should print: 2147483648", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "ABS", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"NEGATE", {
{"positive", "42 NEGATE . CR", "Should print: -42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 NEGATE . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 NEGATE . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"min_int", "-2147483648 NEGATE . CR", "Should print: 2147483648", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "NEGATE", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"MIN", {
{"basic", "5 3 MIN . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"equal", "42 42 MIN . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-5 -3 MIN . CR", "Should print: -5", TEST_NORMAL, 0, 1, {0}},
{"mixed", "-1 1 MIN . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"one_item", "42 MIN", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "MIN", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"MAX", {
{"basic", "5 3 MAX . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"equal", "42 42 MAX . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-5 -3 MAX . CR", "Should print: -3", TEST_NORMAL, 0, 1, {0}},
{"mixed", "-1 1 MAX . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{"one_item", "42 MAX", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "MAX", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"1+", {
{"basic", "5 1+ . CR", "Should print: 6", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 1+ . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 1+ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"chain", "3 1+ 1+ 1+ . CR", "Should print: 6", TEST_NORMAL, 0, 1, {0}},
{"overflow", "2147483647 1+ . CR", "Should overflow", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "1+", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"1-", {
{"basic", "5 1- . CR", "Should print: 4", TEST_NORMAL, 0, 1, {0}},
{"to_zero", "1 1- . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"to_negative", "0 1- . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"chain", "6 1- 1- 1- . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"underflow", "-2147483648 1- . CR", "Should underflow", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "1-", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"2+", {
{"basic", "5 2+ . CR", "Should print: 7", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 2+ . CR", "Should print: 2", TEST_NORMAL, 0, 1, {0}},
{"negative", "-3 2+ . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "2+", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"2-", {
{"basic", "7 2- . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"zero", "2 2- . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "0 2- . CR", "Should print: -2", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "2-", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"2*", {
{"basic", "5 2* . CR", "Should print: 10", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 2* . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-3 2* . CR", "Should print: -6", TEST_NORMAL, 0, 1, {0}},
{"one", "1 2* . CR", "Should print: 2", TEST_NORMAL, 0, 1, {0}},
{"large", "1073741823 2* . CR", "Should print: 2147483646", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "2*", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"2/", {
{"basic", "10 2/ . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"odd", "7 2/ . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 2/ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative_even", "-10 2/ . CR", "Should print: -5", TEST_NORMAL, 0, 1, {0}},
{"one", "1 2/ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "2/", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all arithmetic word test suites
* @param vm Pointer to the Forth virtual machine instance
* @details Runs through all test cases for arithmetic operations including
* basic operations (+, -, *, /), modulo operations (MOD, /MOD),
* and numeric functions (ABS, NEGATE, MIN, MAX)
*/
void run_arithmetic_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Arithmetic Words Tests (Module 5)...");
WordContract mod = {CONTRACT_PHYSICS_TRANSPARENT, 0};
for (int i = 0; arithmetic_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite_m(vm, &arithmetic_word_suites[i], mod);
}
print_module_summary("Arithmetic Words", 0, 0, 0, 0);
}
@@ -0,0 +1,213 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Test suites for FORTH block words (Module 16)
*
* Contains test cases for the following FORTH words:
* - BLOCK: Block access
* - BUFFER: Buffer management
* - UPDATE: Block modification marking
* - SAVE-BUFFERS: Buffer flushing
* - EMPTY-BUFFERS: Buffer invalidation
* - FLUSH: Buffer saving and invalidation
* - LOAD: Block interpretation
* - LIST: Block display
* - SCR: Current block number
* - THRU: Load block range
* - -->: Continue to next block
*/
static WordTestSuite block_word_suites[] = {
{
"BLOCK", {
{"basic", "1 BLOCK DUP . CR", "Should return block address", TEST_NORMAL, 0, 1, {0}},
{"zero_block", "0 BLOCK", "Should handle block 0", TEST_ERROR_CASE, 1, 1, {0}},
{"large_block", "65536 BLOCK", "Should handle large number", TEST_ERROR_CASE, 1, 1, {0}},
{"negative", "-1 BLOCK", "Should handle negative", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "BLOCK", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"BUFFER", {
{"basic", "1 BUFFER DUP . CR", "Should return buffer address", TEST_NORMAL, 0, 1, {0}},
{"zero_block", "0 BUFFER", "Should handle block 0", TEST_ERROR_CASE, 1, 1, {0}},
{"flush_dirty", "2 BLOCK 1+ 2 BUFFER", "Should flush if dirty", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "BUFFER", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"UPDATE", {
{"basic", "1 BLOCK UPDATE", "Should mark block dirty", TEST_NORMAL, 0, 1, {0}},
{"multiple", "1 BLOCK UPDATE UPDATE", "Should handle multiple updates", TEST_NORMAL, 0, 1, {0}},
{"no_block", "0 SCR ! UPDATE", "Should error: blk=0 is invalid", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"SAVE-BUFFERS", {
{"basic", "SAVE-BUFFERS", "Should flush all buffers", TEST_NORMAL, 0, 1, {0}},
{"dirty_blocks", "1 BLOCK UPDATE SAVE-BUFFERS", "Should save dirty blocks", TEST_NORMAL, 0, 1, {0}},
{"no_dirty", "SAVE-BUFFERS", "Should handle no dirty blocks", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"EMPTY-BUFFERS", {
{"basic", "EMPTY-BUFFERS", "Should invalidate all buffers", TEST_NORMAL, 0, 1, {0}},
{"after_use", "1 BLOCK EMPTY-BUFFERS", "Should invalidate used buffers", TEST_NORMAL, 0, 1, {0}},
{"dirty_blocks", "1 BLOCK UPDATE EMPTY-BUFFERS", "Should handle dirty blocks", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"FLUSH", {
{"basic", "FLUSH", "Should save and invalidate buffers", TEST_NORMAL, 0, 1, {0}},
{"dirty_blocks", "1 BLOCK UPDATE FLUSH", "Should flush dirty blocks", TEST_NORMAL, 0, 1, {0}},
{"after_empty", "EMPTY-BUFFERS FLUSH", "Should handle empty buffers", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"LOAD", {
{"basic", "1 LOAD", "Should interpret block 1", TEST_NORMAL, 0, 1, {0}},
{"zero_block", "0 LOAD", "Should handle block 0", TEST_ERROR_CASE, 1, 1, {0}},
{"nonexistent", "9999 LOAD", "Should handle missing block", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "LOAD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"LIST", {
{"basic", "1 LIST", "Should display block 1", TEST_NORMAL, 0, 1, {0}},
{"zero_block", "0 LIST", "Should handle block 0", TEST_ERROR_CASE, 1, 1, {0}},
{"nonexistent", "9999 LIST", "Should handle missing block", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "LIST", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"SCR", {
{"basic", "SCR @ . CR", "Should show current block", TEST_NORMAL, 0, 1, {0}},
{"after_list", "1 LIST SCR @ . CR", "Should update after LIST", TEST_NORMAL, 0, 1, {0}},
{"after_load", "1 LOAD SCR @ . CR", "Should update after LOAD", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"THRU", {
/* Basic functionality */
{"single_block", "1 1 THRU", "Should load single block (1-1)", TEST_NORMAL, 0, 1, {0}},
{"ascending", "1 3 THRU", "Should load blocks 1-3 in order", TEST_NORMAL, 0, 1, {0}},
{"descending", "3 1 THRU", "Should auto-swap and load 1-3", TEST_NORMAL, 0, 1, {0}},
{"same_block", "2 2 THRU", "Should load single block (2-2)", TEST_NORMAL, 0, 1, {0}},
/* Error cases - invalid block numbers */
{"zero_start", "0 3 THRU", "Should reject block 0 as start", TEST_ERROR_CASE, 1, 1, {0}},
{"zero_end", "1 0 THRU", "Should reject block 0 as end", TEST_ERROR_CASE, 1, 1, {0}},
{"both_zero", "0 0 THRU", "Should reject both blocks as 0", TEST_ERROR_CASE, 1, 1, {0}},
{"invalid_start", "9999 10000 THRU", "Should reject invalid start block", TEST_ERROR_CASE, 1, 1, {0}},
{"invalid_end", "1 9999 THRU", "Should reject invalid end block", TEST_ERROR_CASE, 1, 1, {0}},
{"both_invalid", "9998 9999 THRU", "Should reject both invalid blocks", TEST_ERROR_CASE, 1, 1, {0}},
/* Stack underflow */
{"empty_stack", "THRU", "Should cause stack underflow (no args)", TEST_ERROR_CASE, 1, 1, {0}},
{"one_arg", "1 THRU", "Should cause stack underflow (one arg)", TEST_ERROR_CASE, 1, 1, {0}},
/* Range tests */
{"small_range", "1 2 THRU", "Should load 2-block range", TEST_NORMAL, 0, 1, {0}},
{"medium_range", "1 5 THRU", "Should load 5-block range", TEST_NORMAL, 0, 1, {0}},
/* Edge cases */
{"negative_start", "-1 3 THRU", "Should handle negative start", TEST_ERROR_CASE, 1, 1, {0}},
{"negative_end", "1 -3 THRU", "Should handle negative end", TEST_ERROR_CASE, 1, 1, {0}},
{"both_negative", "-3 -1 THRU", "Should handle both negative", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
17, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all block words test suites
* @param vm Pointer to the FORTH virtual machine instance
*
* Runs through all test cases for block manipulation words, including
* block access, buffer management, and block I/O operations. Results
* are logged and summarized after completion.
*/
void run_block_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Block Words Tests (Module 16)...");
for (int i = 0; block_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &block_word_suites[i]);
}
print_module_summary("Block Words", 0, 0, 0, 0);
}
+202
View File
@@ -0,0 +1,202 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "test_runner.h"
#include "../../../include/log.h"
#include "../../../include/vm.h"
/* Report file handle */
static FILE *report_file = NULL;
static time_t test_start_time;
static time_t test_end_time;
static int total_tests_run = 0;
/**
* @brief Initialize markdown report file
*/
static int init_report(void) {
time(&test_start_time);
report_file = fopen("docs/BREAK_ME_REPORT.md", "w");
if (!report_file) {
log_message(LOG_ERROR, "Failed to create BREAK_ME_REPORT.md");
return -1;
}
char time_str[64];
struct tm *tm_info = localtime(&test_start_time);
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
fprintf(report_file, "# 🔥 STARFORTH BREAK-ME DIAGNOSTIC REPORT 🔥\n\n");
fprintf(report_file, "## Executive Summary\n\n");
fprintf(report_file, "**Generated:** %s\n\n", time_str);
fprintf(report_file, "**Test Mode:** ULTRA-COMPREHENSIVE DIAGNOSTIC\n\n");
fprintf(report_file, "**Purpose:** This report documents the results_run_01_2025_12_08 of the most exhaustive ");
fprintf(report_file, "testing suite ever run on StarForth. Every word, every edge case, ");
fprintf(report_file, "every pathological scenario has been tested to ensure rock-solid ");
fprintf(report_file, "reliability and FORTH-79 standard compliance.\n\n");
fprintf(report_file, "---\n\n");
return 0;
}
/**
* @brief Finalize report with system info and easter egg
*/
static void finalize_report(VM *vm) {
time(&test_end_time);
if (!report_file) return;
/* Performance metrics */
fprintf(report_file, "\n## 📊 Performance Metrics\n\n");
fprintf(report_file, "| Metric | Value |\n");
fprintf(report_file, "|--------|-------|\n");
fprintf(report_file, "| Test Duration | %.2f seconds |\n", difftime(test_end_time, test_start_time));
fprintf(report_file, "| Total Tests | %d |\n", total_tests_run);
fprintf(report_file, "| Tests/Second | %.2f |\n\n",
total_tests_run / difftime(test_end_time, test_start_time));
/* System info */
fprintf(report_file, "## 🖥️ System Information\n\n");
fprintf(report_file, "| Component | Specification |\n");
fprintf(report_file, "|-----------|---------------|\n");
fprintf(report_file, "| VM Architecture | Direct-threaded |\n");
fprintf(report_file, "| Standard | FORTH-79 + StarForth Extensions |\n");
fprintf(report_file, "| Cell Size | %zu bytes |\n", sizeof(cell_t));
fprintf(report_file, "| Stack Size | %d cells |\n", STACK_SIZE);
fprintf(report_file, "| Memory | %d bytes |\n\n", VM_MEMORY_SIZE);
/* Easter egg */
fprintf(report_file, "\n---\n\n");
fprintf(report_file, "## 🎉 EASTER EGG: THE STARFORTH CHALLENGE 🎉\n\n");
fprintf(report_file, "```\n");
fprintf(report_file, " ⭐ C O N G R A T U L A T I O N S ⭐\n\n");
fprintf(report_file, " You've discovered the hidden StarForth challenge!\n\n");
fprintf(report_file, " Your system just survived %d exhaustive tests.\n", total_tests_run);
fprintf(report_file, " But can YOU survive the ULTIMATE FORTH CHALLENGE?\n\n");
fprintf(report_file, " 🏆 THE STARFORTH MASTER CHALLENGE 🏆\n\n");
fprintf(report_file, " Write a Forth program that:\n");
fprintf(report_file, " 1. Prints the Fibonacci sequence up to 1000\n");
fprintf(report_file, " 2. Uses ONLY the following words:\n");
fprintf(report_file, " : ; DUP OVER DROP SWAP . CR < IF THEN DO LOOP\n");
fprintf(report_file, " 3. Fits in exactly 79 characters (FORTH-79 tribute!)\n\n");
fprintf(report_file, " Submit your solution to the StarForth community!\n\n");
fprintf(report_file, " Hint: Think recursively, but remember FORTH-79\n");
fprintf(report_file, " doesn't have RECURSE... 😉\n\n");
fprintf(report_file, " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
fprintf(report_file, " BONUS SECRET: If you ran --break-me, you're already\n");
fprintf(report_file, " in the top 1%% of Forth enthusiasts. Welcome to the\n");
fprintf(report_file, " elite club! Your system just proved it can handle\n");
fprintf(report_file, " anything you throw at it.\n\n");
fprintf(report_file, " May your stacks be balanced and your words defined,\n");
fprintf(report_file, " -- The StarForth Development Team ⭐\n");
fprintf(report_file, "```\n\n");
fprintf(report_file, "### 🎮 More Easter Eggs to Find:\n\n");
fprintf(report_file, "1. Try defining a word called `STARSHIP` and see what happens\n");
fprintf(report_file, "2. Run `42 ENTROPY@ .` to see the universe's favorite number\n");
fprintf(report_file, "3. Type `WORDS` and count how many contain 'STAR'\n");
fprintf(report_file, "4. Check what happens when you `FORGET FORGET` (just kidding, don't!)\n\n");
fprintf(report_file, "---\n\n");
fprintf(report_file, "*Generated by StarForth --break-me mode*\n");
fprintf(report_file, "*Copyright (c) 2025 Robert A. James - StarshipOS Forth Project*\n");
fclose(report_file);
report_file = NULL;
}
/**
* @brief Main entry point for --break-me mode
*/
void run_break_me_tests(VM *vm) {
if (!vm) return;
log_message(LOG_INFO, "");
log_message(LOG_INFO, "╔═══════════════════════════════════════════════════════════╗");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "║ 🔥 BREAK-ME MODE ACTIVATED 🔥 ║");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "║ Ultra-Comprehensive StarForth Diagnostic Suite ║");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "║ Preparing to execute EXHAUSTIVE test battery... ║");
log_message(LOG_INFO, "║ Generating detailed markdown report... ║");
log_message(LOG_INFO, "║ Surprise easter egg awaits at the end! ║");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "╚═══════════════════════════════════════════════════════════╝");
log_message(LOG_INFO, "");
/* Initialize report */
if (init_report() != 0) {
log_message(LOG_ERROR, "Failed to initialize report - aborting");
return;
}
/* Run ALL existing test suites */
log_message(LOG_INFO, "Running comprehensive test suite...");
run_all_tests(vm);
/* Update counter from global stats */
extern TestStats global_test_stats;
total_tests_run = global_test_stats.total_tests;
/* Finalize report */
finalize_report(vm);
log_message(LOG_INFO, "");
log_message(LOG_INFO, "╔═══════════════════════════════════════════════════════════╗");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "║ ✅ BREAK-ME COMPLETE ✅ ║");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "║ StarForth survived %d tests! ║", total_tests_run);
log_message(LOG_INFO, "║ Check docs/BREAK_ME_REPORT.md for full details ║");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "║ 🎉 Don't forget to read the easter egg! 🎉 ║");
log_message(LOG_INFO, "║ ║");
log_message(LOG_INFO, "╚═══════════════════════════════════════════════════════════╝");
log_message(LOG_INFO, "");
}
@@ -0,0 +1,267 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/** @brief Test suites for FORTH control words
* @details Contains test cases for all control flow words in Module 12,
* including conditional branches and loop structures
*/
static WordTestSuite control_word_suites[] = {
{
"IF", {
{"true", ": TEST1 IF 42 ELSE 24 THEN ; -1 TEST1 . CR", "Should take true branch", TEST_NORMAL, 0, 1, {0}},
{"false", ": TEST2 IF 42 ELSE 24 THEN ; 0 TEST2 . CR", "Should take false branch", TEST_NORMAL, 0, 1, {0}},
{
"nested", ": TEST3 IF IF 1 ELSE 2 THEN ELSE 3 THEN ; -1 -1 TEST3 . CR", "Should handle nesting",
TEST_NORMAL, 0, 1, {0}
},
{"no_else", ": TEST4 IF 42 THEN ; -1 TEST4 . CR", "Should work without ELSE", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "IF", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"ELSE", {
{"alone", "ELSE", "Should error outside IF", TEST_ERROR_CASE, 1, 1, {0}},
{"double", ": BAD IF 1 ELSE ELSE 2 THEN ;", "Should prevent double ELSE", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"THEN", {
{"alone", "THEN", "Should error outside IF", TEST_ERROR_CASE, 1, 1, {0}},
{"extra", ": BAD IF 1 THEN THEN ;", "Should prevent extra THEN", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"BEGIN", {
{"until", ": TEST5 0 BEGIN 1+ DUP 5 = UNTIL ; TEST5 . CR", "Should loop until true", TEST_NORMAL, 0, 1, {0}},
{
"while", ": TEST6 0 BEGIN DUP 5 < WHILE 1+ REPEAT ; TEST6 . CR", "Should loop while true", TEST_NORMAL, 0, 1, {0}
},
/* TODO: Nested control flow - needs compiler refactoring for proper origin stack */
/* {"nested", ": TEST7 0 BEGIN 1+ BEGIN DUP 3 < WHILE 1+ REPEAT DUP 5 = UNTIL ;", "Should handle nesting", TEST_NORMAL, 0, 1}, */
{"alone", "BEGIN", "Should error outside definition", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"UNTIL", {
{"basic", ": TEST8 BEGIN DUP 1- DUP 0= UNTIL ; 5 TEST8 . CR", "Should count down", TEST_NORMAL, 0, 1, {0}},
{"immediate", ": TEST9 BEGIN 1 UNTIL ; IMMEDIATE", "Should be immediate", TEST_NORMAL, 0, 1, {0}},
{"no_begin", "UNTIL", "Should error without BEGIN", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"WHILE", {
{"basic", ": TEST10 BEGIN DUP 5 < WHILE 1+ REPEAT ; 0 TEST10 . CR", "Should count up", TEST_NORMAL, 0, 1, {0}},
{
"zero_times", ": TEST11 BEGIN DUP 0< WHILE 1+ REPEAT ; 0 TEST11 . CR", "Should handle zero iterations",
TEST_NORMAL, 0, 1, {0}
},
{"no_begin", "WHILE", "Should error without BEGIN", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"REPEAT", {
{
"basic", ": TEST12 BEGIN DUP 5 < WHILE 1+ REPEAT ; 0 TEST12 . CR", "Should terminate loop", TEST_NORMAL, 0, 1, {0}
},
{"no_while", "REPEAT", "Should error without WHILE", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"DO", {
{"basic", ": TEST13 5 0 DO I . LOOP ; TEST13 CR", "Should count 0 to 4", TEST_NORMAL, 0, 1, {0}},
{"negative", ": TEST14 -1 -5 DO I . LOOP ; TEST14 CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"empty", ": TEST15 0 0 DO LOOP ;", "Should handle empty range", TEST_NORMAL, 0, 1, {0}},
{
"nested", ": TEST16 3 0 DO 3 0 DO J I + . LOOP LOOP ; TEST16 CR", "Should handle nesting", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"?DO", {
{"basic", ": TEST17 5 0 ?DO I . LOOP ; TEST17 CR", "Should count 0 to 4", TEST_NORMAL, 0, 1, {0}},
{"skip", ": TEST18 1 1 ?DO I . LOOP ;", "Should skip when equal", TEST_NORMAL, 0, 1, {0}},
{
"nested", ": TEST19 3 0 ?DO 3 I ?DO J I + . LOOP LOOP ; TEST19 CR", "Should handle nesting",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"LOOP", {
{"basic", ": TEST20 5 0 DO I . LOOP ; TEST20 CR", "Should increment by 1", TEST_NORMAL, 0, 1, {0}},
{"no_do", "LOOP", "Should error without DO", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"+LOOP", {
{"basic", ": TEST21 10 0 DO I . 2 +LOOP ; TEST21 CR", "Should count by 2", TEST_NORMAL, 0, 1, {0}},
{"negative", ": TEST22 0 10 DO I . -1 +LOOP ; TEST22 CR", "Should count down", TEST_NORMAL, 0, 1, {0}},
{"variable", ": TEST23 10 0 DO I . DUP +LOOP ; 3 TEST23 CR", "Should use stack value", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"I", {
{"basic", ": TI 4 0 DO I . LOOP CR ; TI", "Should print: 0 1 2 3", TEST_NORMAL, 0, 1, {0}},
{"single", ": TI1 1 0 DO I . LOOP CR ; TI1", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"offset", ": TI2 5 3 DO I . LOOP CR ; TI2", "Should print: 3 4", TEST_NORMAL, 0, 1, {0}},
{"in_expr", ": TI3 3 0 DO I 2 * . LOOP CR ; TI3", "Should print: 0 2 4", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"J", {
{"basic", ": TJ 2 0 DO 3 0 DO J . LOOP LOOP CR ; TJ", "Should print: 0 0 0 1 1 1", TEST_NORMAL, 0, 1, {0}},
{"paired", ": TJ2 2 0 DO 2 0 DO J I + . LOOP LOOP CR ; TJ2", "Should print: 0 1 1 2", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"EXIT", {
{"early_return", ": TEX 5 EXIT 99 ; TEX . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"cond_exit_pos", ": TEX2 DUP 0> IF EXIT THEN NEGATE ; 3 TEX2 . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"cond_exit_neg", ": TEX2 DUP 0> IF EXIT THEN NEGATE ; -4 TEX2 . CR", "Should print: 4", TEST_NORMAL, 0, 1, {0}},
{"no_skipped", ": TEX3 1 2 EXIT 3 ; TEX3 . . CR", "Should print: 2 1", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"LEAVE", {
{"basic", ": TLV 0 5 0 DO I 3 = IF LEAVE THEN 1+ LOOP ; TLV . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"at_start", ": TLV2 0 5 0 DO LEAVE 1+ LOOP ; TLV2 . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"qdloop", ": TLV3 0 5 0 ?DO I 2 = IF LEAVE THEN 1+ LOOP ; TLV3 . CR", "Should print: 2", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"UNLOOP", {
{"early_exit", ": TUL 5 0 DO I 2 = IF UNLOOP 42 EXIT THEN LOOP 99 ; TUL . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"full_loop", ": TUL2 5 0 DO I 9 = IF UNLOOP 77 EXIT THEN LOOP 99 ; TUL2 . CR", "Should print: 99", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"AGAIN", {
{"basic", ": TAG 0 BEGIN 1+ DUP 3 = IF EXIT THEN AGAIN ; TAG . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"countdown",": CD 10 BEGIN 1- DUP 0= IF EXIT THEN AGAIN ; CD . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"CASE", {
{"match_1", ": TC CASE 1 OF 10 ENDOF 2 OF 20 ENDOF 99 SWAP ENDCASE . CR ; 1 TC", "Should print: 10", TEST_NORMAL, 0, 1, {0}},
{"match_2", ": TC CASE 1 OF 10 ENDOF 2 OF 20 ENDOF 99 SWAP ENDCASE . CR ; 2 TC", "Should print: 20", TEST_NORMAL, 0, 1, {0}},
{"default", ": TC CASE 1 OF 10 ENDOF 2 OF 20 ENDOF 99 SWAP ENDCASE . CR ; 5 TC", "Should print: 99", TEST_NORMAL, 0, 1, {0}},
{"zero_case", ": TC CASE 0 OF 88 ENDOF 1 OF 77 ENDOF 66 SWAP ENDCASE . CR ; 0 TC", "Should print: 88", TEST_NORMAL, 0, 1, {0}},
{"neg_case", ": TC CASE -1 OF 55 ENDOF 44 SWAP ENDCASE . CR ; -1 TC", "Should print: 55", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/** @brief Executes all control words test suites
* @param vm Pointer to the FORTH virtual machine instance
* @details Runs through all defined test cases for control words,
* testing conditional branches and loop constructs
*/
void run_control_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Control Words Tests (Module 12)...");
for (int i = 0; control_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &control_word_suites[i]);
run_test_suite(vm, &control_word_suites[i]);
}
print_module_summary("Control Words", 0, 0, 0, 0);
}
@@ -0,0 +1,191 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/** @brief Test suites for Forth defining words (Module 13)
* @details Contains test cases for core defining words including:
* :, ;, CONSTANT, VARIABLE, CREATE, DOES>, [, and ]
*/
static WordTestSuite defining_word_suites[] = {
{
":", {
{"basic", ": TEST1 42 ; TEST1 . CR", "Should define and execute", TEST_NORMAL, 0, 1, {0}},
{"empty_name", ":", "Should handle empty definition", TEST_ERROR_CASE, 1, 1, {0}},
{"nested", ": TEST3 : ;", "Should prevent nested definition", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
";", {
{"alone", ";", "Should error outside definition", TEST_ERROR_CASE, 1, 1, {0}},
{"immediate", ": TEST4 42 ; IMMEDIATE TEST4 . CR", "Should handle immediate", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"CONSTANT", {
{"basic", "42 CONSTANT MEANING MEANING . CR", "Should create constant", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 CONSTANT ZERO ZERO . CR", "Should handle zero", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 CONSTANT MINUS MINUS . CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "CONSTANT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"VARIABLE", {
{"basic", "VARIABLE VAR1 42 VAR1 ! VAR1 @ . CR", "Should create variable", TEST_NORMAL, 0, 1, {0}},
{"multiple", "VARIABLE VAR2 VARIABLE VAR3", "Should create multiple", TEST_NORMAL, 0, 1, {0}},
{"store_fetch", "VARIABLE VAR4 -99 VAR4 ! VAR4 @ . CR", "Should store/fetch", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"CREATE", {
{"basic", "CREATE OBJ1", "Should create word", TEST_NORMAL, 0, 1, {0}},
{"with_data", "CREATE OBJ2 42 , OBJ2 @ . CR", "Should allow data", TEST_NORMAL, 0, 1, {0}},
{"empty_name", "CREATE", "Should handle empty name", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"DOES>", {
{
"basic", ": CONST CREATE , DOES> @ ; 42 CONST MEANING MEANING . CR", "Should define behavior",
TEST_NORMAL, 0, 1, {0}
},
{
"multiple", ": ARRAY CREATE DOES> SWAP CELLS + ; CREATE ARR 10 CELLS ALLOT", "Should work with arrays",
TEST_NORMAL, 0, 1, {0}
},
{"outside", "DOES>", "Should error outside definition", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"[", {
{"basic", ": TEST5 [ 42 ] LITERAL ; TEST5 . CR", "Should enter interpret", TEST_NORMAL, 0, 1, {0}},
{"outside", "[", "Should be allowed outside definition (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"]", {
{"basic", "[ 42 ] . CR", "Should resume compile", TEST_NORMAL, 0, 1, {0}},
{"nested", ": TEST6 [ ] 42 ; TEST6 . CR", "Should handle nesting", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"DEFER", {
{"basic", "DEFER DF1 : DF1W 42 . CR ; ' DF1W IS DF1 DF1", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"uninit", "DEFER DF2 DF2", "Should error: uninitialized deferred word", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"IS", {
{"basic", "DEFER IS1 : IS1W 99 . CR ; ' IS1W IS IS1 IS1", "Should print: 99", TEST_NORMAL, 0, 1, {0}},
{"not_defer", "42 IS DUP", "Should error: DUP is not a DEFER word", TEST_ERROR_CASE, 1, 1, {0}},
{"not_found", "42 IS NOSUCH", "Should error: word not found", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"DEFER@", {
{"basic", "DEFER DFA1 : DFA1W 7 . CR ; ' DFA1W IS DFA1 DEFER@ DFA1 0<> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"not_found", "DEFER@ NOSUCH2", "Should error: word not found", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"[']", {
{"compile_xt", ": BKT1 ['] DUP ; 77 BKT1 EXECUTE . . CR", "Should print: 77 77", TEST_NORMAL, 0, 1, {0}},
{"pin_like", ": BKT2 ['] DROP ; 1 2 BKT2 EXECUTE . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{"not_found", ": BKT3 ['] NOSUCHWORD ;", "Should error: word not found", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/** @brief Executes all test suites for Forth defining words
* @param vm Pointer to the Forth virtual machine instance
* @details Runs through each test suite in defining_word_suites array,
* executing individual test cases and logging results_run_01_2025_12_08
*/
void run_defining_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Defining Words Tests (Module 13)...");
for (int i = 0; defining_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &defining_word_suites[i]);
}
print_module_summary("Defining Words", 0, 0, 0, 0);
}
@@ -0,0 +1,194 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Dictionary Manipulation Words Test Suites - Module 14 */
static WordTestSuite dict_manip_word_suites[] = {
{
"CREATE", {
{"basic", "CREATE test1 42 , test1 @ . CR", "Should create and store", TEST_NORMAL, 0, 1, {0}},
{"empty_name", "CREATE", "Should handle empty name", TEST_ERROR_CASE, 1, 1, {0}},
{
"redefine_shadows", "CREATE T 1 , CREATE T 2 , T @ . CR", "Newest definition should win (prints 2)",
TEST_NORMAL, 0, 1, {0}
},
{"duplicate", "CREATE test2 CREATE test2", "Should allow redefinition (latest shadows)", TEST_NORMAL, 0, 1, {0}},
{"long_name", "CREATE abcdefghijklmnopqrstuvwxyz", "Should handle long name", TEST_EDGE_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"FORGET", {
{"basic", "CREATE temp1 FORGET temp1", "Should forget word", TEST_NORMAL, 0, 1, {0}},
{"nonexistent", "FORGET nonexistent", "Should handle missing word", TEST_ERROR_CASE, 1, 1, {0}},
{"protected", "FORGET FORGET", "Should protect system words", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
/* FENCE ( -- ): raises the FORGET boundary to the current dictionary
* top. Added alongside a real bug fix in FORGET itself, caught while
* building this word -- forgetting the single newest word used to
* incorrectly destroy every other word back to the fence too (a
* use-after-free in the relink logic, not just an off-by-one), a
* SIGSEGV in the worst case. "forget_latest_keeps_predecessor" below
* is exactly the regression case that fix addresses. */
"FENCE", {
{
"basic", "CREATE fnc1 FENCE CREATE fnc2 FORGET fnc2",
"Should forget a word defined after FENCE", TEST_NORMAL, 0, 1, {0}
},
{
"protects_older", "CREATE fnc3 CREATE fnc4 FENCE FORGET fnc3",
"Should refuse to forget a word defined before FENCE", TEST_ERROR_CASE, 1, 1, {0}
},
{
"forget_latest_keeps_predecessor",
"CREATE fnc5 FENCE CREATE fnc6 CREATE fnc7 FORGET fnc7 fnc6 DROP",
"Forgetting the newest word must not disturb an older one still above FENCE",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"IMMEDIATE", {
{"basic", ": test3 42 ; IMMEDIATE test3 . CR", "Should execute immediately", TEST_NORMAL, 0, 1, {0}},
{
"already_immediate", ": test4 43 ; IMMEDIATE IMMEDIATE", "Should handle double immediate", TEST_NORMAL, 0, 1, {0}
},
/* TODO: IMMEDIATE outside definition - requires compile-state tracking in IMMEDIATE */
/* {"outside_def", "IMMEDIATE", "Should error outside definition", TEST_ERROR_CASE, 1, 1}, */
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"FIND", {
{"existing", "FIND DUP . CR", "Should find system word", TEST_NORMAL, 0, 1, {0}},
{"user_word", ": test5 44 ; FIND test5 . CR", "Should find user word", TEST_NORMAL, 0, 1, {0}},
{"nonexistent", "FIND nonexistent . CR", "Should return 0", TEST_NORMAL, 0, 1, {0}},
{"empty", "FIND", "Should error (no token)", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"DEFINITIONS", {
{"basic", "FORTH DEFINITIONS", "Should set current to context", TEST_NORMAL, 0, 1, {0}},
{"multiple", "FORTH DEFINITIONS DEFINITIONS", "Should be idempotent", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"SMUDGE", {
{"during_def", ": test6 SMUDGE 45 ;", "Should hide incomplete def", TEST_NORMAL, 0, 1, {0}},
{"outside_def", "SMUDGE", "Should error outside def", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"LATEST", {
{"basic", "LATEST @ . CR", "Should show latest word", TEST_NORMAL, 0, 1, {0}},
{"after_def", ": test7 46 ; LATEST @ . CR", "Should update after def", TEST_NORMAL, 0, 1, {0}},
{
"after_forget", "CREATE test8 FORGET test8 LATEST @ . CR", "Should update after forget", TEST_NORMAL, 0,
1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
">BODY", {
{"basic", "CREATE test9 ' test9 >BODY . CR", "Should get param field", TEST_NORMAL, 0, 1, {0}},
{"variable", "VARIABLE var1 ' var1 >BODY . CR", "Should get var storage", TEST_NORMAL, 0, 1, {0}},
{"nonexistent", "' NONEXISTENT >BODY", "Should handle missing word", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"HIDDEN", {
{"basic", ": test10 HIDDEN 47 ;", "Should hide word", TEST_NORMAL, 0, 1, {0}},
{"outside_def", "HIDDEN", "Should error outside def", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all dictionary manipulation words test suites
* @param vm Pointer to the Forth virtual machine instance
* @details Runs through all test cases for CREATE, FORGET, IMMEDIATE, FIND,
* DEFINITIONS, SMUDGE, LATEST, >BODY, and HIDDEN words
*/
void run_dictionary_manipulation_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Dictionary Manipulation Words Tests (Module 14)...");
for (int i = 0; dict_manip_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &dict_manip_word_suites[i]);
}
print_module_summary("Dictionary Manipulation Words", 0, 0, 0, 0);
}
@@ -0,0 +1,216 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Dictionary Words Test Suites - Module 4 */
static WordTestSuite dictionary_word_suites[] = {
{
"HERE", {
{"basic", "HERE HERE = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"after_comma", "HERE 42 , HERE SWAP - 1 CELLS = 0 SWAP /", "Asserts HERE advances by exactly 1 CELLS (8 bytes on 64-bit)", TEST_NORMAL, 0, 1, {0}},
{"after_c_comma", "HERE 65 C, HERE SWAP - . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{"after_allot", "HERE 10 ALLOT HERE SWAP - . CR", "Should print: 10", TEST_NORMAL, 0, 1, {0}},
{"stability", "HERE DUP HERE = . CR", "Should be stable", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"ALLOT", {
{"basic", "HERE 10 ALLOT HERE SWAP - . CR", "Should print: 10", TEST_NORMAL, 0, 1, {0}},
{"zero", "HERE 0 ALLOT HERE SWAP - . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "HERE -4 ALLOT HERE SWAP - . CR", "Should print: -4", TEST_NORMAL, 0, 1, {0}},
{"large", "HERE 100 ALLOT HERE SWAP - . CR", "Should print: 100", TEST_NORMAL, 0, 1, {0}},
{"after_use", "ALIGN HERE 10 ALLOT 42 OVER ! @ . CR", "Should store and retrieve", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "ALLOT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
",", {
{"basic", "42 , HERE 1 CELLS - @ . CR", "Should compile 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-999 , HERE 1 CELLS - @ . CR", "Should compile -999", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 , HERE 1 CELLS - @ . CR", "Should compile 0", TEST_NORMAL, 0, 1, {0}},
{"max_int", "2147483647 , HERE 1 CELLS - @ . CR", "Should compile max int", TEST_EDGE_CASE, 0, 1, {0}},
{"min_int", "-2147483648 , HERE 1 CELLS - @ . CR", "Should compile min int", TEST_EDGE_CASE, 0, 1, {0}},
{"multiple", "10 , 20 , HERE 1 CELLS - @ . HERE 1 CELLS - @ . CR", "Should compile multiple", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", ",", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"C,", {
{"basic", "65 C, HERE 1 - C@ . CR", "Should compile byte 65", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 C, HERE 1 - C@ . CR", "Should compile byte 0", TEST_NORMAL, 0, 1, {0}},
{"high_byte", "255 C, HERE 1 - C@ . CR", "Should compile byte 255", TEST_NORMAL, 0, 1, {0}},
{"truncation", "256 C, HERE 1 - C@ . CR", "Should truncate to 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 C, HERE 1 - C@ . CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"multiple", "65 C, 66 C, HERE 2 - C@ . HERE 1 - C@ . CR", "Should compile multiple", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "C,", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"2,", {
{"basic", "12345 67890 2, HERE 2 CELLS - 2@ . . CR", "Should compile double", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0 2, HERE 2 CELLS - 2@ . . CR", "Should compile zero double", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1000 -2000 2, HERE 2 CELLS - 2@ . . CR", "Should compile negative double", TEST_NORMAL, 0, 1, {0}},
{"large", "2147483647 -1 2, HERE 2 CELLS - 2@ . . CR", "Should compile large double", TEST_EDGE_CASE, 0, 1, {0}},
{"one_item", "42 2,", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "2,", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"PAD", {
{"basic", "PAD PAD = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"different_from_here", "PAD HERE = . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"stability", "PAD DUP PAD = . CR", "Should be stable", TEST_NORMAL, 0, 1, {0}},
{"usable", "PAD 42 OVER ! @ . CR", "Should be usable memory", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"SP@", {
// DISABLED: flaky on some architectures, non-deterministic
{"basic", "SP@ SP@ = . CR", "Should print: 0 (different after push)", TEST_NORMAL, 0, 0, {0}},
{"depth_effect", "42 SP@ SWAP DROP SP@ = . CR", "Should show stack effect", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "DEPTH 0= SP@ AND . CR", "Should work on empty stack", TEST_NORMAL, 0, 1, {0}},
{
"multiple_items", "1 2 3 SP@ SWAP DROP SWAP DROP SWAP DROP SP@ = . CR", "Should track changes",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"SP!", {
{"basic_restore", "1 2 3 SP@ 4 5 6 DROP DROP DROP SP! DEPTH .", "Should restore stack", TEST_NORMAL, 0, 1, {0}},
{"invalid_addr", "0 SP!", "Should cause error", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "SP!", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"LATEST", {
{"basic", "LATEST LATEST = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"stability", "LATEST DUP LATEST = . CR", "Should be stable", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"COMPILATION", {
{
"here_comma_sequence", "HERE 10 , 20 , 30 , HERE SWAP - 12 = . CR", "Should advance HERE by 12",
TEST_NORMAL, 0, 1, {0}
},
{
"mixed_compilation", "HERE 42 , 65 C, 100 200 2, HERE SWAP - 13 = . CR", "Should advance correctly",
TEST_NORMAL, 0, 1, {0}
}, // MAY FAIL on non-4-byte platforms
{
"allot_comma_combo", "HERE 10 ALLOT 99 , HERE SWAP - 14 = . CR", "Should combine allot and comma",
TEST_NORMAL, 0, 1, {0}
},
{"pad_isolation", "PAD 999 OVER ! HERE 42 , PAD @ 999 = . CR", "PAD should be isolated", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"STACK_MGMT", {
{"sp_round_trip", "42 43 SP@ >R 44 45 R> SP! . . CR", "Should print: 43 42", TEST_NORMAL, 0, 1, {0}},
{
"depth_preservation", "1 2 3 DEPTH >R SP@ >R 4 5 6 R> SP! R> DEPTH = . CR", "Should preserve depth",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all dictionary word test suites
*
* @param vm Pointer to the FORTH virtual machine instance
*
* This function runs through all dictionary word test suites, including tests for:
* - HERE (dictionary pointer)
* - ALLOT (memory allocation)
* - Comma operations (, C, 2,)
* - PAD (scratch pad area)
* - Stack pointer operations (SP@ SP!)
* - LATEST (most recent dictionary entry)
* - Compilation sequences
* - Stack management operations
*/
void run_dictionary_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Dictionary Words Tests (Module 4)...");
for (int i = 0; dictionary_word_suites[i].word_name != NULL; i++) {
run_test_suite(vm, &dictionary_word_suites[i]);
}
print_module_summary("Dictionary Words", 0, 0, 0, 0);
}
@@ -0,0 +1,346 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Double Number Words Test Suites - Module 6 */
static WordTestSuite double_word_suites[] = {
{
"2DROP", {
{"basic", "1 2 3 4 2DROP . . CR", "Should print: 1 2", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 42 99 2DROP . . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 -2 100 200 2DROP . . CR", "Should print: -1 -2", TEST_NORMAL, 0, 1, {0}},
{"exact_four", "10 20 30 40 2DROP DEPTH . CR", "Should print: 2", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "2DROP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 2DROP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"2DUP", {
{"basic", "100 200 2DUP . . . . CR", "Should print: 100 200 100 200", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 2DUP . . . . CR", "Should print: 0 0 0 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-100 -200 2DUP . . . . CR", "Should duplicate negatives", TEST_NORMAL, 0, 1, {0}},
{"depth_check", "10 20 2DUP DEPTH . CR", "Should print: 4", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "2DUP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 2DUP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"2OVER", {
{"basic", "10 20 30 40 2OVER . . . . . . CR", "Should print: 10 20 30 40 10 20", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 99 88 2OVER . . . . . . CR", "Should print: 0 0 99 88 0 0", TEST_NORMAL, 0, 1, {0}},
{"mixed", "-1 2 -3 4 2OVER . . . . . . CR", "Should handle mixed signs", TEST_NORMAL, 0, 1, {0}},
{"depth_check", "1 2 3 4 2OVER DEPTH . CR", "Should print: 6", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 2OVER", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "2OVER", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"2SWAP", {
{"basic", "10 20 30 40 2SWAP . . . . CR", "Should print: 30 40 10 20", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 99 88 2SWAP . . . . CR", "Should print: 99 88 0 0", TEST_NORMAL, 0, 1, {0}},
{"same_pairs", "42 42 42 42 2SWAP . . . . CR", "Should print: 42 42 42 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-10 -20 30 40 2SWAP . . . . CR", "Should print: 30 40 -10 -20", TEST_NORMAL, 0, 1, {0}},
{"depth_check", "1 2 3 4 2SWAP DEPTH . CR", "Should print: 4", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 2SWAP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "2SWAP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"2ROT", {
{"basic", "10 20 30 40 50 60 2ROT . . . . . . CR", "Should print: 30 40 50 60 10 20", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 1 1 2 2 2ROT . . . . . . CR", "Should rotate with zeros", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 -2 3 4 -5 -6 2ROT . . . . . . CR", "Should handle negatives", TEST_NORMAL, 0, 1, {0}},
{"depth_check", "1 2 3 4 5 6 2ROT DEPTH . CR", "Should print: 6", TEST_NORMAL, 0, 1, {0}},
{"five_items", "1 2 3 4 5 2ROT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "2ROT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"D+", {
{"basic", "100 200 300 400 D+ . . CR", "Should print: 400 600", TEST_NORMAL, 0, 1, {0}},
{"zero_add", "42 99 0 0 D+ . . CR", "Should print: 42 99", TEST_NORMAL, 0, 1, {0}},
{"negative", "-100 -200 50 75 D+ . . CR", "Should handle negatives", TEST_NORMAL, 0, 1, {0}},
{"carry", "2147483647 0 1 0 D+ . . CR", "Should handle carry", TEST_EDGE_CASE, 0, 1, {0}},
{"both_negative", "-100 -200 -300 -400 D+ . . CR", "Should add negatives", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 D+", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "D+", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"D-", {
{"basic", "500 600 100 200 D- . . CR", "Should print: 400 400", TEST_NORMAL, 0, 1, {0}},
{"zero_sub", "42 99 0 0 D- . . CR", "Should print: 42 99", TEST_NORMAL, 0, 1, {0}},
{"from_zero", "0 0 100 200 D- . . CR", "Should print: -100 -200", TEST_NORMAL, 0, 1, {0}},
{"negative", "100 200 -50 -75 D- . . CR", "Should handle negatives", TEST_NORMAL, 0, 1, {0}},
{"borrow", "0 0 1 0 D- . . CR", "Should handle borrow", TEST_EDGE_CASE, 0, 1, {0}},
{"same_values", "100 200 100 200 D- . . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 D-", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "D-", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
8, {0}
},
{
"DNEGATE", {
{"positive", "100 200 DNEGATE . . CR", "Should negate positive double", TEST_NORMAL, 0, 1, {0}},
{"negative", "-100 -200 DNEGATE . . CR", "Should negate negative double", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0 DNEGATE . . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"max_positive", "2147483647 0 DNEGATE . . CR", "Should negate max positive", TEST_EDGE_CASE, 0, 1, {0}},
{"one_item", "42 DNEGATE", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "DNEGATE", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"DABS", {
{"positive", "100 200 DABS . . CR", "Should print: 100 200", TEST_NORMAL, 0, 1, {0}},
{"negative", "-100 -200 DABS . . CR", "Should print: 100 200", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0 DABS . . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"mixed_sign", "100 -1 DABS . . CR", "Should handle mixed sign", TEST_NORMAL, 0, 1, {0}},
{"min_double", "-2147483648 -1 DABS . . CR", "Should handle min double", TEST_EDGE_CASE, 0, 1, {0}},
{"one_item", "42 DABS", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "DABS", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"DMIN", {
{"basic", "100 200 300 400 DMIN . . CR", "Should print: 100 200", TEST_NORMAL, 0, 1, {0}},
{"equal", "42 99 42 99 DMIN . . CR", "Should print: 42 99", TEST_NORMAL, 0, 1, {0}},
{"negative", "-100 -200 -50 -75 DMIN . . CR", "Should find min negative", TEST_NORMAL, 0, 1, {0}},
{"mixed", "-100 -1 100 0 DMIN . . CR", "Should handle mixed signs", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 DMIN", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "DMIN", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"DMAX", {
{"basic", "100 200 300 400 DMAX . . CR", "Should print: 300 400", TEST_NORMAL, 0, 1, {0}},
{"equal", "42 99 42 99 DMAX . . CR", "Should print: 42 99", TEST_NORMAL, 0, 1, {0}},
{"negative", "-300 -400 -100 -200 DMAX . . CR", "Should find max negative", TEST_NORMAL, 0, 1, {0}},
{"mixed", "-100 -1 100 0 DMAX . . CR", "Should handle mixed signs", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 DMAX", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "DMAX", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"D=", {
{"equal", "100 200 100 200 D= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"not_equal_low", "100 200 101 200 D= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"not_equal_high", "100 200 100 201 D= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"zero_equal", "0 0 0 0 D= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative_equal", "-100 -200 -100 -200 D= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 D=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "D=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"D<", {
{"less_than", "100 200 300 400 D< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"greater_than", "300 400 100 200 D< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"equal", "100 200 100 200 D< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative_positive", "-100 -1 100 0 D< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative_negative", "-300 -1 -100 -1 D< . CR", "Should compare negatives", TEST_NORMAL, 0, 1, {0}},
{"high_word_diff", "100 200 100 300 D< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"three_items", "1 2 3 D<", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "D<", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
8, {0}
},
{
"D0=", {
{"zero", "0 0 D0= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"positive", "100 200 D0= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-100 -200 D0= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"zero_high", "0 100 D0= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"zero_low", "100 0 D0= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"one_item", "42 D0=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "D0=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"D0<", {
{"negative", "-100 -1 D0< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0 D0< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"positive", "100 0 D0< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative_high_only", "100 -1 D0< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"large_negative", "-1 -1 D0< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"one_item", "42 D0<", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "D0<", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"S>D", {
{"positive", "5 S>D . . CR", "Should sign-extend positive: 0 5", TEST_NORMAL, 0, 1, {0}},
{"negative", "-5 S>D . . CR", "Should sign-extend negative: -1 -5", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 S>D . . CR", "Should sign-extend zero: 0 0", TEST_NORMAL, 0, 1, {0}},
{"max_int", "2147483647 S>D . . CR", "Should extend max int: 0 2147483647", TEST_EDGE_CASE, 0, 1, {0}},
{"min_int", "-2147483648 S>D . . CR","Should extend min int: -1 -2147483648", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "S>D", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"D2*", {
{"basic", "5 0 D2* . . CR", "Should double 5: 0 10", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0 D2* . . CR", "Should double zero: 0 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 -1 D2* . . CR", "Should double -1: -1 -2", TEST_NORMAL, 0, 1, {0}},
{"carry", "0 1 D2* . . CR", "Should shift high cell: 0 2", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "D2*", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"D2/", {
{"basic", "10 0 D2/ . . CR", "Should halve 10: 0 5", TEST_NORMAL, 0, 1, {0}},
{"negative", "-2 -1 D2/ . . CR", "Should halve -2: -1 -1", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0 D2/ . . CR", "Should halve zero: 0 0", TEST_NORMAL, 0, 1, {0}},
{"odd", "3 0 D2/ . . CR", "Should truncate toward zero: 0 1", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "D2/", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
/* NOTE: 2>R/2R>/2R@ are tested at REPL level (not inside compiled words).
* The execution loop stores the IP in return_stack[rsp]; if 2>R were called
* inside a colon definition, it would increment rsp and the loop would
* dereference the pushed data value as a pointer — same model as >R tests. */
"2>R", {
{"roundtrip", "10 20 2>R 2R> . . CR", "Should store and retrieve: 20 10", TEST_NORMAL, 0, 1, {0}},
{"depth_after", "1 2 2>R DEPTH . 2R> DROP DROP CR", "Should empty data stack: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "2>R", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"2R>", {
{"roundtrip", "5 6 2>R 2R> . . CR", "Should retrieve double: 6 5", TEST_NORMAL, 0, 1, {0}},
{"depth", "9 8 2>R 2R> DEPTH . CR", "Should push 2 items to data stack: 2", TEST_NORMAL, 0, 1, {0}},
{"empty_rstack","2R>", "Should cause return stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"2R@", {
{"peek", "3 4 2>R 2R@ . . 2R> DROP DROP CR", "Should peek: 4 3", TEST_NORMAL, 0, 1, {0}},
{"non_destructive", "7 8 2>R 2R@ DROP DROP 2R> . . CR", "Should leave rstack intact", TEST_NORMAL, 0, 1, {0}},
{"empty_rstack", "2R@", "Should cause return stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all test suites for double number words
*
* This function runs comprehensive tests for all double number word operations,
* including stack manipulation (2DROP, 2DUP, etc.), arithmetic operations (D+, D-, etc.),
* and comparison operations (D=, D<, etc.).
*
* @param vm Pointer to the virtual machine instance where tests will be executed
*/
void run_double_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Double Number Words Tests (Module 6)...");
for (int i = 0; double_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &double_word_suites[i]);
}
print_module_summary("Double Number Words", 0, 0, 0, 0);
}
@@ -0,0 +1,170 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Test suites for format words (Module 10)
* Contains test cases for number base conversion and formatting operations
*/
static WordTestSuite format_word_suites[] = {
{
"BASE", {
{"decimal", "DECIMAL 42 . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"hex", "HEX FF . CR", "Should print: FF", TEST_NORMAL, 0, 1, {0}},
{"octal", "OCTAL 52 . CR", "Should print: 52", TEST_NORMAL, 0, 1, {0}},
{"base_store", "16 BASE ! 255 . CR", "Should print: FF", TEST_NORMAL, 0, 1, {0}},
{"base_fetch", "BASE @ . CR", "Should print current base", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"DECIMAL", {
{"from_hex", "HEX FF DECIMAL . CR", "Should print: 255", TEST_NORMAL, 0, 1, {0}},
{"from_octal", "OCTAL 52 DECIMAL . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"state_persist", "DECIMAL 42 . CR 42 . CR HEX 42 . CR", "Should maintain base", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"HEX", {
{"from_decimal", "DECIMAL 255 HEX . CR", "Should print: FF", TEST_NORMAL, 0, 1, {0}},
{"from_octal", "OCTAL 52 HEX . CR", "Should print: 2A", TEST_NORMAL, 0, 1, {0}},
{"state_persist", "HEX FF . CR DECIMAL 255 . CR", "Should maintain base", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"OCTAL", {
{"from_decimal", "DECIMAL 42 OCTAL . CR", "Should print: 52", TEST_NORMAL, 0, 1, {0}},
{"from_hex", "HEX 2A OCTAL . CR", "Should print: 52", TEST_NORMAL, 0, 1, {0}},
{"state_persist", "DECIMAL 42 . CR OCTAL 52 . CR", "Should maintain base", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"<#", {
{"basic", "DECIMAL 42 S>D <# #S #> TYPE CR", "Should format number", TEST_NORMAL, 0, 1, {0}},
{"empty", "0 <# #> TYPE CR", "Should handle zero", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 <# #S #> TYPE CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"#", {
{"single_digit", "15 <# # #> TYPE CR", "Should print rightmost digit", TEST_NORMAL, 0, 1, {0}},
{"multiple", "15 <# # # #> TYPE CR", "Should print digits right to left", TEST_NORMAL, 0, 1, {0}},
{"zero_pad", "5 <# # 0 # #> TYPE CR", "Should handle zero padding", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"#S", {
{"basic", "42 <# #S #> TYPE CR", "Should convert all digits", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 <# #S #> TYPE CR", "Should handle zero", TEST_NORMAL, 0, 1, {0}},
{"large", "1234567890 <# #S #> TYPE CR", "Should handle large numbers", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"SIGN", {
{"negative", "-42 ABS <# #S SIGN #> TYPE CR", "Should add minus", TEST_NORMAL, 0, 1, {0}},
{"positive", "42 <# #S SIGN #> TYPE CR", "Should not add sign", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 <# #S SIGN #> TYPE CR", "Should handle zero", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"#>", {
{"normal", "42 <# #S #> TYPE CR", "Should terminate formatting", TEST_NORMAL, 0, 1, {0}},
{"empty", "0 <# #> TYPE CR", "Should handle empty format", TEST_NORMAL, 0, 1, {0}},
{"stack_effect", "42 <# #S #> SWAP . . CR", "Should leave addr u", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"HOLD", {
{"basic", "42 <# 46 HOLD #S #> TYPE CR", "Should insert dot", TEST_NORMAL, 0, 1, {0}},
{"overflow", "<# 257 HOLD #>", "Should handle overflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all format words test suites
* @param vm Pointer to the Forth virtual machine instance
* @details Runs through all test cases for number base manipulation and formatting words
*/
void run_format_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Format Words Tests (Module 10)...");
for (int i = 0; format_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &format_word_suites[i]);
}
print_module_summary("Format Words", 0, 0, 0, 0);
}
@@ -0,0 +1,139 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* inference_words_test.c — POST tests for the SSM inference engine's
* proof-covered accessor/primitive words (Module 26)
*
* inference_words.c registers 20 words; only 8 are proof-covered per
* proof/COVERAGE.md ("5 accessors full; rest guard/shape") — this file
* covers exactly those 8, not the L8 Jacquard / Bayesian-posterior words,
* which COVERAGE.md does not claim coverage for. No test file existed for
* any word in this module before.
*
* The three array-based primitives (Q.VARIANCE/INFER-DECAY-SLOPE/
* INFER-WINDOW-WIDTH) need real data in VM memory — HERE is used as
* scratch, matching the idiom memory_words_test.c already uses. The four
* *-@ accessors and INFER-RUN are exercised in one suite (INFER-WINDOW@)
* that runs INFER-RUN first, since the accessors just read
* vm->last_inference_outputs and INFER-RUN is what populates it.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Inference Engine Words Test Suites - Module 26 */
static WordTestSuite inference_word_suites[] = {
{
"Q.VARIANCE", {
{"three_cells", "100 HERE ! 200 HERE 8 + ! 300 HERE 16 + ! HERE 3 Q.VARIANCE Q.TO-INT . CR",
"Should print population variance of {100,200,300} (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-DECAY-SLOPE", {
{"three_cells", "300 HERE ! 200 HERE 8 + ! 100 HERE 16 + ! HERE 3 INFER-DECAY-SLOPE Q.TO-INT . CR",
"Should print decay slope for a decreasing trajectory (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-WINDOW-WIDTH", {
{"three_cells", "100 HERE ! 200 HERE 8 + ! 300 HERE 16 + ! HERE 3 INFER-WINDOW-WIDTH . CR",
"Should print an inferred window width (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-RUN", {
{"basic", "INFER-RUN", "Should run without error", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-WINDOW@", {
{"after_run", "INFER-RUN INFER-WINDOW@ . CR", "Should print last adaptive_window_width (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-DECAY@", {
{"after_run", "INFER-RUN INFER-DECAY@ Q.TO-INT . CR", "Should print last adaptive_decay_slope (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-VARIANCE@", {
{"after_run", "INFER-RUN INFER-VARIANCE@ Q.TO-INT . CR", "Should print last window_variance_q48 (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-FIT@", {
{"after_run", "INFER-RUN INFER-FIT@ Q.TO-INT . CR", "Should print last slope_fit_quality_q48 (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"INFER-EARLY-EXIT@", {
{"after_run", "INFER-RUN INFER-EARLY-EXIT@ . CR", "Should print 0 or 1 (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes the proof-covered subset of inference-engine word test suites
* @param vm Pointer to the Forth virtual machine instance
* @details Covers the 8 words proof/COVERAGE.md marks as proof-covered in
* inference_words.c: the three array-based primitives (Q.VARIANCE,
* INFER-DECAY-SLOPE, INFER-WINDOW-WIDTH), INFER-RUN, and the four
* *-@ output accessors (INFER-WINDOW@/DECAY@/VARIANCE@/FIT@) plus
* INFER-EARLY-EXIT@. Does not cover the L8 Jacquard or Bayesian
* posterior words in the same file — not claimed as proof-covered.
*/
void run_inference_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Inference Engine Words Tests (Module 26)...");
/* CONTRACT_NONE, not CONTRACT_PHYSICS_TRANSPARENT: these words read
* physics state (rolling window, dictionary heat) by design -- A4'
* transparency is a property they deliberately do not have. */
WordContract mod = {CONTRACT_NONE, 0};
for (int i = 0; inference_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite_m(vm, &inference_word_suites[i], mod);
}
print_module_summary("Inference Engine Words", 0, 0, 0, 0);
}
@@ -0,0 +1,233 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file integration_tests.c
* @brief Full program integration tests for StarForth
*
* This file contains integration tests that verify complete Forth programs
* as regression tests. It includes test suites for complete programs,
* data structures, and algorithms implementations.
*/
#include "test_common.h"
#include "test_runner.h"
/* Integration test suites - complete Forth programs */
static WordTestSuite integration_suites[] = {
{
"COMPLETE_PROGRAMS", {
{
"prime_check",
": PRIME? ( n -- flag ) "
" DUP 2 < IF DROP 0 EXIT THEN "
" DUP 2 = IF DROP -1 EXIT THEN "
" DUP 2 MOD 0= IF DROP 0 EXIT THEN "
" DUP 3 DO "
" DUP I MOD 0= IF DROP 0 EXIT THEN "
" 2 +LOOP "
" DROP -1 "
"; "
"17 PRIME? . CR",
"Prime number checker",
TEST_NORMAL, 0, 1, {0}
},
{
"array_sum",
": ARRAY 5 CELLS ALLOT ; "
"ARRAY NUMS "
": STORE-NUMS "
" 10 NUMS 0 CELLS + ! "
" 20 NUMS 1 CELLS + ! "
" 30 NUMS 2 CELLS + ! "
" 40 NUMS 3 CELLS + ! "
" 50 NUMS 4 CELLS + ! "
"; "
": SUM-NUMS "
" 0 5 0 DO "
" NUMS I CELLS + @ + "
" LOOP "
"; "
"STORE-NUMS SUM-NUMS . CR",
"Array sum program",
TEST_NORMAL, 0, 1, {0}
},
{
"fizzbuzz_mini",
": FIZZ? 3 MOD 0= ; "
": BUZZ? 5 MOD 0= ; "
": FIZZBUZZ "
" DUP FIZZ? OVER BUZZ? AND IF DROP .\" FizzBuzz \" EXIT THEN "
" DUP FIZZ? IF DROP .\" Fizz \" EXIT THEN "
" DUP BUZZ? IF DROP .\" Buzz \" EXIT THEN "
" . "
"; "
"15 FIZZBUZZ CR",
"FizzBuzz for 15",
TEST_NORMAL, 0, 1, {0}
},
{
"calculator",
": + DEPTH 2 < IF .\" Stack underflow\" CR EXIT THEN + ; "
": CALC "
" 5 3 + "
" 2 * "
" 4 - "
" . CR "
"; "
"CALC",
"Simple calculator",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"DATA_STRUCTURES", {
{
"linked_list",
"VARIABLE HEAD "
"0 HEAD ! "
": NODE ( value -- addr ) "
" HERE "
" , " /* value */
" HEAD @ , " /* next pointer */
" DUP HEAD ! "
"; "
": PRINT-LIST ( -- ) "
" HEAD @ "
" BEGIN DUP WHILE "
" DUP @ . "
" CELL+ @ "
" REPEAT DROP CR "
"; "
"10 NODE DROP "
"20 NODE DROP "
"30 NODE DROP "
"PRINT-LIST",
"Simple linked list",
TEST_NORMAL, 0, 1, {0}
},
{
"stack_min_max",
": INIT-MINMAX "
" 2147483647 VARIABLE MIN " /* Max int */
" -2147483648 VARIABLE MAX " /* Min int */
"; "
": UPDATE-MINMAX ( n -- ) "
" DUP MIN @ < IF DUP MIN ! ELSE DROP THEN "
" DUP MAX @ > IF DUP MAX ! ELSE DROP THEN "
"; "
"INIT-MINMAX "
"42 UPDATE-MINMAX "
"17 UPDATE-MINMAX "
"99 UPDATE-MINMAX "
"MIN @ . MAX @ . CR",
"Min/Max tracker",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"ALGORITHMS", {
{
"bubble_sort_concept",
": SWAP-IF-NEEDED ( addr1 addr2 -- ) "
" OVER @ OVER @ > IF "
" OVER @ OVER @ "
" ROT ! SWAP ! "
" ELSE 2DROP THEN "
"; "
"VARIABLE A "
"VARIABLE B "
"5 A ! 3 B ! "
"A B SWAP-IF-NEEDED "
"A @ . B @ . CR",
"Swap if needed (sort helper)",
TEST_NORMAL, 0, 1, {0}
},
{
"gcd",
": GCD ( a b -- gcd ) "
" BEGIN DUP WHILE "
" TUCK MOD "
" REPEAT DROP "
"; "
"48 18 GCD . CR",
"Greatest Common Divisor",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
};
/**
* @brief Executes all integration test suites for StarForth
*
* @param vm Pointer to the Forth virtual machine instance
*
* This function runs through all defined integration test suites, including:
* - Complete Programs suite (prime checker, array operations, etc)
* - Data Structures suite (linked list, min/max tracker)
* - Algorithms suite (sorting, GCD)
*/
void run_integration_tests(VM *vm) {
if (!vm) return;
log_message(LOG_INFO, "==============================================");
log_message(LOG_INFO, " StarForth Integration Test Suite");
log_message(LOG_INFO, " Complete Forth Programs");
log_message(LOG_INFO, "==============================================\n");
for (size_t i = 0; i < sizeof(integration_suites) / sizeof(WordTestSuite); i++) {
run_test_suite(vm, &integration_suites[i]);
}
log_message(LOG_INFO, "\n==============================================");
log_message(LOG_INFO, " Integration Tests Complete");
log_message(LOG_INFO, "==============================================\n");
}
+285
View File
@@ -0,0 +1,285 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Test suites for I/O words (Module 9)
* @details Contains comprehensive test cases for all I/O related FORTH words
* including output formatting, character I/O, and base conversion operations
*/
static WordTestSuite io_word_suites[] = {
{
".", {
{"positive", "42 . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 . CR", "Should print: -42", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"max_int", "2147483647 . CR", "Should print: 2147483647", TEST_EDGE_CASE, 0, 1, {0}},
{"min_int", "-2147483648 . CR", "Should print: -2147483648", TEST_EDGE_CASE, 0, 1, {0}},
{"multiple", "1 2 3 . . . CR", "Should print: 3 2 1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", ".", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"CR", {
{"basic", "42 . CR 43 . CR", "Should print on separate lines", TEST_NORMAL, 0, 1, {0}},
{"multiple", "CR CR CR", "Should print multiple newlines", TEST_NORMAL, 0, 1, {0}},
{"no_stack_effect", "DEPTH CR DEPTH = . CR", "Should not affect stack", TEST_NORMAL, 0, 1, {0}},
{"after_output", "1 2 3 . . . CR", "Should work after output", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"SPACE", {
{"basic", "42 . SPACE 43 . CR", "Should add space between numbers", TEST_NORMAL, 0, 1, {0}},
{"multiple", "SPACE SPACE SPACE", "Should print multiple spaces", TEST_NORMAL, 0, 1, {0}},
{"no_stack_effect", "DEPTH SPACE DEPTH = . CR", "Should not affect stack", TEST_NORMAL, 0, 1, {0}},
{"with_text", "65 EMIT SPACE 66 EMIT CR", "Should space between chars", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"SPACES", {
{"zero", "0 SPACES", "Should print no spaces", TEST_NORMAL, 0, 1, {0}},
{"one", "1 SPACES", "Should print one space", TEST_NORMAL, 0, 1, {0}},
{"multiple", "5 SPACES", "Should print five spaces", TEST_NORMAL, 0, 1, {0}},
{"large", "20 SPACES", "Should print twenty spaces", TEST_NORMAL, 0, 1, {0}},
{"negative", "-5 SPACES", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"with_output", "42 . 3 SPACES 43 . CR", "Should space output", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "SPACES", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"EMIT", {
{"letter_A", "65 EMIT CR", "Should print: A", TEST_NORMAL, 0, 1, {0}},
{"letter_Z", "90 EMIT CR", "Should print: Z", TEST_NORMAL, 0, 1, {0}},
{"digit", "48 EMIT CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"space_char", "32 EMIT CR", "Should print space", TEST_NORMAL, 0, 1, {0}},
{"newline", "10 EMIT", "Should print newline", TEST_NORMAL, 0, 1, {0}},
{"high_ascii", "127 EMIT CR", "Should print DEL char", TEST_NORMAL, 0, 1, {0}},
{"truncation", "321 EMIT CR", "Should truncate to low byte", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 EMIT CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"sequence", "72 EMIT 73 EMIT 10 EMIT", "Should print: HI", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "EMIT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
10, {0}
},
{
"KEY", {
{"basic_test", "KEY DROP", "Should wait for keypress", TEST_NORMAL, 0, 0, {0}},
{"echo_test", "KEY DUP EMIT CR", "Should echo keypresses", TEST_NORMAL, 0, 0, {0}},
{"multiple", "KEY KEY KEY DROP DROP DROP", "Should read multiple keys", TEST_NORMAL, 0, 0, {0}},
{"no_stack_underflow", "KEY . CR", "Should push key to stack", TEST_NORMAL, 0, 0, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"?KEY", {
{"available_test", "?KEY . CR", "Should test key availability", TEST_NORMAL, 0, 0, {0}},
{"no_wait", "?KEY IF KEY EMIT THEN CR", "Should not wait", TEST_NORMAL, 0, 0, {0}},
{"multiple_check", "?KEY ?KEY OR . CR", "Should check availability", TEST_NORMAL, 0, 0, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"TYPE", {
{
"basic_string", "HERE S\" Hello\" DUP >R HERE SWAP CMOVE HERE R> TYPE CR", "Should print: Hello",
TEST_NORMAL, 0, 1, {0}
},
{"empty_string", "HERE 0 TYPE CR", "Should print nothing", TEST_NORMAL, 0, 1, {0}},
{"single_char", "HERE 65 OVER C! 1 TYPE CR", "Should print: A", TEST_NORMAL, 0, 1, {0}},
{
"numbers", "HERE S\" 12345\" DUP >R HERE SWAP CMOVE HERE R> TYPE CR", "Should print: 12345",
TEST_NORMAL, 0, 1, {0}
},
{"zero_length", "PAD 0 TYPE", "Should handle zero length", TEST_NORMAL, 0, 1, {0}},
{"one_item", "PAD TYPE", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "TYPE", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"COUNT", {
{
"basic", "HERE S\" Test\" DROP C@ HERE 1+ SWAP COUNT . . CR", "Should show length and addr+1",
TEST_NORMAL, 0, 1, {0}
},
{"empty", "HERE 0 OVER C! COUNT . . CR", "Should handle empty string", TEST_NORMAL, 0, 1, {0}},
{"max_length", "HERE 255 OVER C! COUNT . . CR", "Should handle max length", TEST_NORMAL, 0, 1, {0}},
{"zero_addr_plus_one", "HERE COUNT SWAP 1- = . CR", "Should increment address", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "COUNT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"EXPECT", {
{"basic_input", "PAD 10 EXPECT", "Should read up to 10 chars", TEST_NORMAL, 0, 0, {0}},
{"max_length", "PAD 80 EXPECT", "Should read up to 80 chars", TEST_NORMAL, 0, 0, {0}},
{"zero_length", "PAD 0 EXPECT", "Should handle zero length", TEST_NORMAL, 0, 1, {0}},
{"one_item", "PAD EXPECT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "EXPECT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"SPAN", {
{"after_expect", "PAD 10 EXPECT SPAN @ . CR", "Should show chars read", TEST_NORMAL, 0, 0, {0}},
{"zero_input", "PAD 0 EXPECT SPAN @ . CR", "Should show zero", TEST_NORMAL, 0, 1, {0}},
{"variable_test", "SPAN @ SPAN @ = . CR", "Should be consistent", TEST_NORMAL, 0, 1, {0}},
{"address_test", "SPAN 0<> . CR", "Should be valid address", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"WORD", {
/* Basic functionality */
{"basic_parse", "BL WORD COUNT TYPE CR", "Should parse next word", TEST_NORMAL, 0, 0, {0}},
{"delimiter_test", "44 WORD COUNT TYPE CR", "Should use comma delimiter", TEST_NORMAL, 0, 0, {0}},
{"empty_input", "BL WORD COUNT . CR", "Should handle empty input", TEST_NORMAL, 0, 1, {0}},
/* Different delimiters */
{"space_delim", "32 WORD DROP", "Should use space as delimiter", TEST_NORMAL, 0, 1, {0}},
{"newline_delim", "10 WORD DROP", "Should use newline as delimiter", TEST_NORMAL, 0, 1, {0}},
{"tab_delim", "9 WORD DROP", "Should use tab as delimiter", TEST_NORMAL, 0, 1, {0}},
{"comma_delim", "44 WORD DROP", "Should use comma as delimiter", TEST_NORMAL, 0, 1, {0}},
/* Leading delimiters */
{"skip_leading", "BL WORD DROP", "Should skip leading spaces", TEST_NORMAL, 0, 1, {0}},
/* Word length tests */
{"single_char", "BL WORD COUNT . CR", "Should parse single character", TEST_NORMAL, 0, 1, {0}},
{"long_word", "BL WORD COUNT 0 > . CR", "Should parse long word", TEST_NORMAL, 0, 1, {0}},
/* Edge cases */
{"zero_delim", "0 WORD DROP", "Should handle null delimiter", TEST_NORMAL, 0, 1, {0}},
{"high_ascii", "127 WORD DROP", "Should handle DEL delimiter", TEST_NORMAL, 0, 1, {0}},
/* Count-prefixed string */
{"count_format", "BL WORD C@ . CR", "Should have count byte", TEST_NORMAL, 0, 1, {0}},
{"count_value", "BL WORD COUNT SWAP DROP . CR", "Should return count", TEST_NORMAL, 0, 1, {0}},
/* Error cases */
{"empty_stack", "WORD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
/* Consecutive delimiters */
{"multi_delim", "BL WORD DROP BL WORD DROP", "Should handle multiple parses", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
17, {0}
},
/* Base conversion and formatting tests */
{
"BASE_OUTPUT", {
{"decimal", "10 BASE ! 255 . CR", "Should print: 255", TEST_NORMAL, 0, 1, {0}},
{"hex", "16 BASE ! 255 . CR", "Should print: FF", TEST_NORMAL, 0, 1, {0}},
{"octal", "8 BASE ! 64 . CR", "Should print: 100", TEST_NORMAL, 0, 1, {0}},
{"binary", "5 2 BASE ! . CR", "Should print: 101", TEST_NORMAL, 0, 1, {0}},
{
"base_restore", "BASE @ DECIMAL 255 16 BASE ! . BASE ! CR", "Should restore base", TEST_NORMAL, 0,
1, {0}
},
{"reset_decimal", "DECIMAL", "Reset base to DECIMAL for subsequent tests", TEST_NORMAL, 0, 0, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
/* Output formatting combinations */
{
"FORMAT_COMBO", {
{"number_space_number", "DECIMAL 42 . SPACE 43 . CR", "Should format: 42 43", TEST_NORMAL, 0, 1, {0}},
{"multi_line", "DECIMAL 1 . CR 2 . CR 3 . CR", "Should print on separate lines", TEST_NORMAL, 0, 1, {0}},
{"indented", "DECIMAL 5 SPACES 42 . CR", "Should indent number", TEST_NORMAL, 0, 1, {0}},
{"table_format", "DECIMAL 1 . 3 SPACES 10 . 3 SPACES 100 . CR", "Should format table", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all I/O words test suites
* @param vm Pointer to the FORTH virtual machine instance
* @details Runs through all defined test suites for I/O words, logging results_run_01_2025_12_08
* and providing a summary of the test execution
*/
void run_io_words_tests(VM *vm) {
log_message(LOG_INFO, "Running I/O Words Tests (Module 9)...");
for (int i = 0; io_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &io_word_suites[i]);
}
print_module_summary("I/O Words", 0, 0, 0, 0);
}
@@ -0,0 +1,306 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Logical Words Test Suites - Module 8 */
static WordTestSuite logical_word_suites[] = {
{
"AND", {
{"both_true", "-1 -1 AND . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"first_false", "0 -1 AND . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"second_false", "-1 0 AND . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"both_false", "0 0 AND . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"bitwise", "85 51 AND . CR", "Should print: 17", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "AND", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 AND", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"OR", {
{"both_true", "-1 -1 OR . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"first_false", "0 -1 OR . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"second_false", "-1 0 OR . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"both_false", "0 0 OR . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"bitwise", "85 51 OR . CR", "Should print: 119", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "OR", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 OR", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"XOR", {
{"both_true", "-1 -1 XOR . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"first_false", "0 -1 XOR . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"second_false", "-1 0 XOR . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"both_false", "0 0 XOR . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"bitwise", "85 51 XOR . CR", "Should print: 102", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "XOR", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 XOR", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"NOT", {
{"true", "-1 NOT . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"false", "0 NOT . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"positive", "42 NOT . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 NOT . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "NOT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"=", {
{"equal", "42 42 = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"not_equal", "42 43 = . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"zero_equal", "0 0 = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative_equal", "-42 -42 = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative_positive", "-42 42 = . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 =", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"<>", {
{"not_equal", "42 43 <> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"equal", "42 42 <> . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"zero_equal", "0 0 <> . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 42 <> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "<>", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 <>", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"<", {
{"less_than", "5 7 < . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"greater_than", "7 5 < . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"equal", "5 5 < . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative_positive", "-5 5 < . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative_negative", "-7 -5 < . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "<", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 <", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
">", {
{"greater_than", "7 5 > . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"less_than", "5 7 > . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"equal", "5 5 > . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"positive_negative", "5 -5 > . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative_negative", "-5 -7 > . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", ">", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 >", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"0=", {
{"zero", "0 0= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"positive", "42 0= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 0= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "0=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"0<", {
{"negative", "-42 0< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"positive", "42 0< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "0<", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"0>", {
{"positive", "42 0> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0> . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 0> . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "0>", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"0<>", {
{"nonzero_pos", "42 0<> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"nonzero_neg", "-1 0<> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 0<> . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "0<>", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"U<", {
{"less", "3 5 U< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"equal", "5 5 U< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"greater", "7 5 U< . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"zero_one", "0 1 U< . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "U<", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"U>", {
{"greater", "5 3 U> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"equal", "5 5 U> . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"less", "3 5 U> . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"one_zero", "1 0 U> . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "U>", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"WITHIN", {
{"in_range", "3 1 5 WITHIN . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"at_low", "1 1 5 WITHIN . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"at_high", "5 1 5 WITHIN . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"below", "0 1 5 WITHIN . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"above", "6 1 5 WITHIN . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "WITHIN", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"TRUE", {
{"value", "TRUE . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"as_flag", "TRUE 0= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"in_and", "TRUE TRUE AND . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"FALSE", {
{"value", "FALSE . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"as_flag", "FALSE 0= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"in_or", "TRUE FALSE OR . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
">=", {
{"less", "3 5 >= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"greater", "5 3 >= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"equal", "3 3 >= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 0 >= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"neg_neg", "-1 -2 >= . CR","Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"underflow", ">=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"<=", {
{"less", "3 5 <= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"greater", "5 3 <= . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"equal", "3 3 <= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 0 <= . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"neg_neg", "-2 -1 <= . CR","Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"underflow", "<=", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all logical words test suites
*
* This function runs comprehensive tests for all FORTH logical and comparison operators,
* including stack underflow checks and edge cases.
*
* @param vm Pointer to the FORTH virtual machine instance
*/
void run_logical_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Logical Words Tests (Module 8)...");
WordContract mod = {CONTRACT_PHYSICS_TRANSPARENT, 0};
for (int i = 0; logical_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite_m(vm, &logical_word_suites[i], mod);
}
print_module_summary("Logical Words", 0, 0, 0, 0);
}
@@ -0,0 +1,274 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file mama_forth_words_test.c
* @brief Test module for Mama FORTH vocabulary (M7.1 Capsule System)
*
* Tests for the MAMA vocabulary words. These words are kernel-only
* (__STARKERNEL__), so in hosted builds all tests are marked as
* not implemented and will be skipped.
*
* In kernel builds, these tests validate:
* - Capsule directory enumeration
* - Descriptor field access
* - VM birth protocol
* - Experiment execution
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/*
* Mama FORTH words (CAPSULE-COUNT etc.) are kernel runtime objects, not
* FORTH dictionary words. They do not live in the StarForth dictionary,
* so there is nothing to test via vm_interpret. Mark all tests skipped.
*/
#define MAMA_IMPLEMENTED 0
/* Mama FORTH Words Test Suites */
static WordTestSuite mama_word_suites[] = {
{
"CAPSULE-COUNT",
{
{
"basic",
"CAPSULE-COUNT . CR",
"Should print number of capsules",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"non_negative",
"CAPSULE-COUNT 0 >= . CR",
"Should be >= 0",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"CAPSULE@",
{
{
"first_capsule",
"0 CAPSULE@ . CR",
"Should return first descriptor address",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"out_of_bounds",
"9999 CAPSULE@ . CR",
"Should return 0 for invalid index",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"empty_stack",
"CAPSULE@",
"Should cause stack underflow",
TEST_ERROR_CASE, 1, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"CAPSULE-FLAGS@",
{
{
"basic",
"0 CAPSULE@ CAPSULE-FLAGS@ . CR",
"Should return flags from descriptor",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"null_desc",
"0 CAPSULE-FLAGS@ . CR",
"Should handle null descriptor",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"empty_stack",
"CAPSULE-FLAGS@",
"Should cause stack underflow",
TEST_ERROR_CASE, 1, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"CAPSULE-LEN@",
{
{
"basic",
"0 CAPSULE@ CAPSULE-LEN@ . CR",
"Should return payload length",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"empty_stack",
"CAPSULE-LEN@",
"Should cause stack underflow",
TEST_ERROR_CASE, 1, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"CAPSULE-HASH@",
{
{
"basic",
"0 CAPSULE@ CAPSULE-HASH@ . CR",
"Should return content hash",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"empty_stack",
"CAPSULE-HASH@",
"Should cause stack underflow",
TEST_ERROR_CASE, 1, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"MAMA-VM-ID",
{
{
"always_zero",
"MAMA-VM-ID . CR",
"Should always print 0",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"equals_zero",
"MAMA-VM-ID 0 = . CR",
"Should equal 0",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"VM-COUNT",
{
{
"at_least_one",
"VM-COUNT . CR",
"Should be at least 1 (Mama)",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"positive",
"VM-COUNT 0 > . CR",
"Should be positive",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"CAPSULE-BIRTH",
{
{
"invalid_id",
"9999999 CAPSULE-BIRTH . CR",
"Should return -1 for invalid capsule ID",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{
"empty_stack",
"CAPSULE-BIRTH",
"Should cause stack underflow",
TEST_ERROR_CASE, 1, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"CAPSULE-RUN",
{
{
"empty_stack",
"CAPSULE-RUN",
"Should cause stack underflow",
TEST_ERROR_CASE, 1, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"CAPSULE-TEST",
{
{
"basic",
"CAPSULE-TEST",
"Should print diagnostic message",
TEST_NORMAL, 0, MAMA_IMPLEMENTED, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}} /* End marker */
};
/**
* @brief Executes all Mama FORTH vocabulary test suites
*
* In hosted builds, all tests will be skipped since MAMA_IMPLEMENTED=0.
* In kernel builds, tests will execute against the capsule system.
*
* @param vm Pointer to the Forth virtual machine instance
*/
void run_mama_forth_words_tests(VM *vm)
{
log_message(LOG_INFO, "Running Mama FORTH Words Tests (Capsule System M7.1)...");
#ifndef __STARKERNEL__
log_message(LOG_INFO, " (Hosted build - Mama words not available, tests will be skipped)");
#endif
for (int i = 0; mama_word_suites[i].word_name != NULL; i++) {
run_test_suite(vm, &mama_word_suites[i]);
}
print_module_summary("Mama FORTH Words", 0, 0, 0, 0);
}
@@ -0,0 +1,240 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Memory Words Test Suites - Module 3 */
static WordTestSuite memory_word_suites[] = {
{
"!", {
{"basic", "42 HERE ! HERE @ . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 HERE ! HERE @ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-999 HERE ! HERE @ . CR", "Should print: -999", TEST_NORMAL, 0, 1, {0}},
{"overwrite", "111 HERE ! 222 HERE ! HERE @ . CR", "Should print: 222", TEST_NORMAL, 0, 1, {0}},
{"max_int", "2147483647 HERE ! HERE @ . CR", "Should store max int", TEST_EDGE_CASE, 0, 1, {0}},
{"min_int", "-2147483648 HERE ! HERE @ . CR", "Should store min int", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "!", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 !", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
8, {0}
},
{
"@", {
{"after_store", "123 HERE ! HERE @ . CR", "Should print: 123", TEST_NORMAL, 0, 1, {0}},
{"multiple_reads", "456 HERE ! HERE @ HERE @ = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "@", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"C!", {
{"basic", "65 HERE C! HERE C@ . CR", "Should print: 65", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 HERE C! HERE C@ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"high_byte", "255 HERE C! HERE C@ . CR", "Should print: 255", TEST_NORMAL, 0, 1, {0}},
{"truncation", "256 HERE C! HERE C@ . CR", "Should print: 0 (truncated)", TEST_EDGE_CASE, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"C@", {
{"after_cstore", "97 HERE C! HERE C@ . CR", "Should print: 97", TEST_NORMAL, 0, 1, {0}},
{"zero_byte", "0 HERE C! HERE C@ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
",", {
{"basic", "42 , HERE 8 - @ . CR", "Should compile 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-999 , HERE 8 - @ . CR", "Should compile -999", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"C,", {
{"basic", "65 C, HERE 1 - C@ . CR", "Should compile byte 65", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 C, HERE 1 - C@ . CR", "Should compile byte 0", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"2,", {
{
"roundtrip", "12345 67890 2, HERE 16 - 2@ . . CR", "Should print: 67890 12345",
TEST_NORMAL, 0, 1, {0}
},
{
"save-and-verify", "12345 67890 2, HERE 16 - 2@ 67890 = SWAP 12345 = AND . CR",
"Should print: -1", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"HERE", {
{"basic", "HERE HERE = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"after_comma", "HERE 42 , HERE SWAP - 1 CELLS = 0 SWAP /", "Asserts HERE advances by exactly 1 CELLS (8 bytes on 64-bit)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"ALLOT", {
{"basic", "HERE 10 ALLOT HERE SWAP - . CR", "Should print: 10", TEST_NORMAL, 0, 1, {0}},
{"zero", "HERE 0 ALLOT HERE SWAP - . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "HERE -4 ALLOT HERE SWAP - . CR", "Should print: -4", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"PAD", {
{"basic", "PAD PAD = . CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"different_from_here", "PAD HERE = . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"+!", {
{"basic", "10 HERE ! 5 HERE +! HERE @ . CR", "Should print: 15", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "42 HERE ! 0 HERE +! HERE @ . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "10 HERE ! -3 HERE +! HERE @ . CR", "Should print: 7", TEST_NORMAL, 0, 1, {0}},
{"accumulate", "0 HERE ! 1 HERE +! 2 HERE +! 3 HERE +! HERE @ . CR", "Should print: 6", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "+!", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"-!", {
{"basic", "10 HERE ! 3 HERE -! HERE @ . CR", "Should print: 7", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "42 HERE ! 0 HERE -! HERE @ . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "10 HERE ! -5 HERE -! HERE @ . CR", "Should print: 15", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "-!", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"2!", {
{"basic", "11 22 HERE 2! HERE @ . HERE 8 + @ . CR", "Should print: 22 11", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 HERE 2! HERE @ . HERE 8 + @ . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-1 -2 HERE 2! HERE @ . HERE 8 + @ . CR", "Should print: -2 -1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "2!", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"FILL", {
{"basic", "HERE 8 65 FILL HERE C@ . HERE 1+ C@ . CR", "Should print: 65 65", TEST_NORMAL, 0, 1, {0}},
{"zero_n", "HERE 0 88 FILL", "Should do nothing", TEST_NORMAL, 0, 1, {0}},
{"zero_byte", "HERE 4 0 FILL HERE C@ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "FILL", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"MOVE", {
{"basic", "HERE 65 OVER C! HERE 1+ 66 OVER C! HERE HERE 16 + 2 MOVE HERE 16 + C@ . HERE 17 + C@ . CR", "Should print: 65 66", TEST_NORMAL, 0, 1, {0}},
{"zero_n", "HERE HERE 8 + 0 MOVE", "Should do nothing", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "MOVE", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"ERASE", {
{"basic", "HERE 4 65 FILL HERE 4 ERASE HERE C@ . HERE 3 + C@ . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"zero_n", "HERE 0 ERASE", "Should do nothing", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "ERASE", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes the test suite for memory manipulation words
* @details Runs through a series of test cases for Forth memory operations including
* store, fetch, allot, and other memory-related words
*
* @param vm Pointer to the Forth virtual machine instance
*/
void run_memory_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Memory Words Tests (Module 3)...");
WordContract mod = {CONTRACT_PHYSICS_TRANSPARENT, 0};
for (int i = 0; memory_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite_m(vm, &memory_word_suites[i], mod);
}
print_module_summary("Memory Words", 0, 0, 0, 0);
}
@@ -0,0 +1,162 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Module 7 test-suite table for mixed-arithmetic FORTH-79 words.
*
* Covers the five combined multiply-divide words: @c *-/ , @c *-/MOD ,
* @c M+ , @c M* , and @c M/MOD . Each suite exercises normal cases,
* edge cases (overflow, large products), and error cases (division by
* zero, stack underflow). The table is terminated with a NULL sentinel.
*/
static WordTestSuite mixed_arithmetic_word_suites[] = {
{
"*/", {
// ( n1 n2 n3 -- n4 ) n1 * n2 / n3 (integer math)
{"basic", "6 7 4 */ . CR", "Should print: 10 (6*7/4)", TEST_NORMAL, 0, 1, {0}},
{"exact_division", "12 5 3 */ . CR", "Should print: 20 (12*5/3)", TEST_NORMAL, 0, 1, {0}},
{"zero_multiply", "0 999 123 */ . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"by_one", "42 5 1 */ . CR", "Should print: 210 (42*5/1)", TEST_NORMAL, 0, 1, {0}},
{"truncation", "7 3 2 */ . CR", "Should print: 10 (21/2 truncated)", TEST_NORMAL, 0, 1, {0}},
{"negative_multiply", "-6 7 4 */ . CR", "Should print: -10", TEST_NORMAL, 0, 1, {0}},
{"negative_divisor", "6 7 -4 */ . CR", "Should print: -10", TEST_NORMAL, 0, 1, {0}},
{"div_by_zero", "6 7 0 */", "Should cause division by zero", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "*/", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"two_items", "1 2 */", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
10, {0}
},
{
"*/MOD", {
// ( n1 n2 n3 -- rem quot ) (n1*n2)/n3 -> quot, rem
{"basic", "17 3 5 */MOD . . CR", "Should print: 1 10 (51/5 = 10 rem 1)", TEST_NORMAL, 0, 1, {0}},
{"exact_division", "15 4 3 */MOD . . CR", "Should print: 0 20 (60/3 = 20 rem 0)", TEST_NORMAL, 0, 1, {0}},
{"zero_multiply", "0 999 123 */MOD . . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"by_one", "42 5 1 */MOD . . CR", "Should print: 0 210", TEST_NORMAL, 0, 1, {0}},
{"remainder", "7 8 5 */MOD . . CR", "Should print: 1 11 (56/5 = 11 rem 1)", TEST_NORMAL, 0, 1, {0}},
{"negative", "-7 3 4 */MOD . . CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"div_by_zero", "6 7 0 */MOD", "Should cause division by zero", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "*/MOD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
8, {0}
},
{
"M+", {
// ( d_lo d_hi n -- d_lo' d_hi' ) Add n to double d (low first, then high, then n)
{"basic", "0 100 200 M+ . . CR", "Should print: 200 100", TEST_NORMAL, 0, 1, {0}},
{"overflow", "0 2147483647 1 M+ . . CR", "Should handle overflow", TEST_EDGE_CASE, 0, 1, {0}},
{"negative", "0 -100 50 M+ . . CR", "Should print: 50 -100", TEST_NORMAL, 0, 1, {0}},
{"zero_add", "0 42 0 M+ . . CR", "Should print: 0 42", TEST_NORMAL, 0, 1, {0}},
{"both_negative", "0 -100 -200 M+ . . CR", "Should print: -200 -100", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "M+", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 M+", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"M*", {
// ( n1 n2 -- d_lo d_hi ) signed multiply, returns double (lo first, hi second)
{"basic", "123 456 M* . . CR", "Should print: 56088 0", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "999 0 M* . . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"by_one", "42 1 M* . . CR", "Should print: 42 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-123 456 M* . . CR", "Should print: -56088 -1", TEST_NORMAL, 0, 1, {0}},
{"large_multiply", "65536 65536 M* . . CR", "Should handle large result", TEST_EDGE_CASE, 0, 1, {0}},
{"both_negative", "-100 -200 M* . . CR", "Should print: 20000 0", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "M*", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 M*", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
8, {0}
},
{
"M/MOD", {
// ( d_lo d_hi n -- rem quot ) double/single signed division (lo first, then hi, then n)
{"basic", "0 1000 100 M/MOD . . CR", "Should print: 0 10 (1000/100)", TEST_NORMAL, 0, 1, {0}},
{"with_remainder", "0 1234 100 M/MOD . . CR", "Should handle remainder", TEST_NORMAL, 0, 1, {0}},
{"double_dividend", "5001 0 10 M/MOD . . CR", "Should print: 1 500 (5001/10)", TEST_NORMAL, 0, 1, {0}},
{"by_one", "0 42 1 M/MOD . . CR", "Should print: 0 42", TEST_NORMAL, 0, 1, {0}},
{"negative_dividend", "-1 1000 100 M/MOD . . CR", "Should handle negative", TEST_NORMAL, 0, 1, {0}},
{"div_by_zero", "0 100 0 M/MOD", "Should cause division by zero", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "M/MOD", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Module 7 entry point — run all mixed-arithmetic word POST tests.
*
* Iterates @c mixed_arithmetic_word_suites and delegates each to
* @c run_test_suite(). Covers five word families:
*
* - @c *-/ ( n1 n2 n3 -- n4 ) — scale: n1*n2/n3 in 64-bit intermediate
* - @c *-/MOD ( n1 n2 n3 -- rem quot ) — scale with remainder
* - @c M+ ( d n -- d' ) — add single to double
* - @c M* ( n1 n2 -- d ) — signed multiply to double
* - @c M/MOD ( d n -- rem quot ) — double divided by single
*
* @param vm VM instance; must be initialised with the standard vocabulary.
*/
void run_mixed_arithmetic_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Mixed Arithmetic Words Tests (Module 7)...");
for (int i = 0; mixed_arithmetic_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &mixed_arithmetic_word_suites[i]);
}
print_module_summary("Mixed Arithmetic Words", 0, 0, 0, 0);
}
@@ -0,0 +1,127 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* physics_freeze_words_test.c — POST tests for the proof-covered subset of
* physics_freeze_words.c (Module 27)
*
* physics_freeze_words.c registers 9 words; proof/COVERAGE.md and
* proof/StarForth_Physics_Freeze_Words.thy give real (non-"True"-placeholder)
* lemmas for exactly 6 of them: FREEZE-WORD, UNFREEZE-WORD, FROZEN?, HEAT!,
* HEAT@, DECAY-RATE@. FREEZE-CRITICAL, SHOW-HEAT, and ALL-HEATS carry only
* trivial "not_modelled"/pass-through True lemmas, so they are intentionally
* left uncovered here (matches the 6/9 figure in COVERAGE.md). No test file
* for this module existed before.
*
* Each test case defines a scratch dictionary word (`:` ... `;`) and takes
* its name via S" — every word here operates on ( caddr u ), the standard
* counted-string-less pair the rest of the FORTH interpreter uses. Using
* `run_test_suite` (no WordContract) rather than `run_test_suite_m`, matching
* defining_words_tests.c's precedent: these test cases define dictionary
* words, and check_physics_transparent's own contract explicitly documents
* that its re-run mechanism does not snapshot/restore dictionary state.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Physics Freeze Words Test Suites - Module 27 */
static WordTestSuite physics_freeze_word_suites[] = {
{
"FREEZE-WORD", {
{"basic",
": __freeze_test_word__ ; S\" __freeze_test_word__\" FREEZE-WORD S\" __freeze_test_word__\" FROZEN? . CR",
"Should print: -1 (word is now frozen)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"UNFREEZE-WORD", {
{"basic",
": __unfreeze_test_word__ ; S\" __unfreeze_test_word__\" FREEZE-WORD S\" __unfreeze_test_word__\" UNFREEZE-WORD S\" __unfreeze_test_word__\" FROZEN? . CR",
"Should print: 0 (freeze cleared)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"FROZEN?", {
{"not_frozen",
": __frozen_test_word__ ; S\" __frozen_test_word__\" FROZEN? . CR",
"Should print: 0 (freshly defined word is not frozen)", TEST_NORMAL, 0, 1, {0}},
{"not_found",
"S\" __nonexistent_freeze_xyz__\" FROZEN? . CR",
"Should print: 0 (not-found treated as not frozen)", TEST_EDGE_CASE, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"HEAT!", {
{"basic",
": __heat_store_test_word__ ; 999 S\" __heat_store_test_word__\" HEAT! S\" __heat_store_test_word__\" HEAT@ . CR",
"Should print: 999", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"HEAT@", {
{"of_fresh_word",
": __heat_fetch_test_word__ ; S\" __heat_fetch_test_word__\" HEAT@ . CR",
"Should print: 0 (freshly defined word has zero heat)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"DECAY-RATE@", {
{"basic", "DECAY-RATE@ . CR",
"Should print the compile-time DECAY_RATE_PER_US_Q16 constant (no error)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes the proof-covered subset of physics-freeze word test suites
* @param vm Pointer to the Forth virtual machine instance
* @details Covers the 6 words proof/StarForth_Physics_Freeze_Words.thy gives
* real lemmas for: FREEZE-WORD, UNFREEZE-WORD, FROZEN?, HEAT!,
* HEAT@, DECAY-RATE@. Does not cover FREEZE-CRITICAL, SHOW-HEAT, or
* ALL-HEATS — those carry only trivial/not-modelled lemmas in the
* same theory file.
*/
void run_physics_freeze_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Physics Freeze Words Tests (Module 27)...");
for (int i = 0; physics_freeze_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &physics_freeze_word_suites[i]);
}
print_module_summary("Physics Freeze Words", 0, 0, 0, 0);
}
+230
View File
@@ -0,0 +1,230 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* q48_words_test.c — POST tests for Q48.16 fixed-point words (Module 25)
*
* Proof-covered per proof/COVERAGE.md (StarForth_Q48_Words.thy, 17/23) but
* had no interpreter-level POST coverage at all before this file — no test
* file for q48_words.c existed. Values are built with Q.FROM-INT/Q.1/Q.0
* and read back with Q.TO-INT so results print as plain readable integers;
* pass/fail itself is judged only by vm->error (matching every other
* WordTestSuite-based module), so exact printed values are for a human
* reading the log, not machine-verified — same convention arithmetic_words_
* test.c already uses for its own "Should print: N" fields.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* Q48.16 Words Test Suites - Module 25 */
static WordTestSuite q48_word_suites[] = {
{
"Q.+", {
{"basic", "1 Q.FROM-INT 2 Q.FROM-INT Q.+ Q.TO-INT . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.-", {
{"basic", "5 Q.FROM-INT 2 Q.FROM-INT Q.- Q.TO-INT . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.*", {
{"basic", "3 Q.FROM-INT 4 Q.FROM-INT Q.* Q.TO-INT . CR", "Should print: 12", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q./", {
{"basic", "10 Q.FROM-INT 2 Q.FROM-INT Q./ Q.TO-INT . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"by_zero", "5 Q.FROM-INT 0 Q.FROM-INT Q./ Q.TO-INT . CR", "Should print: 0 (division by zero yields q=0)", TEST_EDGE_CASE, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"Q.ABS", {
{"basic", "3 Q.FROM-INT Q.NEG Q.ABS Q.TO-INT . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.NEG", {
{"basic", "5 Q.FROM-INT Q.NEG Q.TO-INT . CR", "Should print: -5", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.LOG", {
{"of_one", "Q.1 Q.LOG Q.TO-INT . CR", "Should print: 0 (ln 1.0 = 0)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.EXP", {
{"of_zero", "Q.0 Q.EXP Q.TO-INT . CR", "Should print: 1 (e^0 = 1)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.SQRT", {
{"of_four", "4 Q.FROM-INT Q.SQRT Q.TO-INT . CR", "Should print: 2", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.SIN", {
{"of_zero", "Q.0 Q.SIN Q.TO-INT . CR", "Should print: 0 (sin 0 = 0)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.COS", {
{"of_zero", "Q.0 Q.COS Q.TO-INT . CR", "Should print: 1 (cos 0 = 1)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.FROM-INT", {
{"round_trip", "7 Q.FROM-INT Q.TO-INT . CR", "Should print: 7", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.TO-INT", {
{"of_one", "Q.1 Q.TO-INT . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.1", {
{"raw_value", "Q.1 . CR", "Should print: 65536", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.0", {
{"raw_value", "Q.0 . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.SCALE", {
{"raw_value", "Q.SCALE . CR", "Should print: 65536", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.=", {
{"equal", "Q.1 Q.1 Q.= . CR", "Should print: -1 (true)", TEST_NORMAL, 0, 1, {0}},
{"not_equal", "Q.1 Q.0 Q.= . CR", "Should print: 0 (false)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"Q.<", {
{"true", "Q.0 Q.1 Q.< . CR", "Should print: -1 (true)", TEST_NORMAL, 0, 1, {0}},
{"false", "Q.1 Q.0 Q.< . CR", "Should print: 0 (false)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"Q.>", {
{"true", "Q.1 Q.0 Q.> . CR", "Should print: -1 (true)", TEST_NORMAL, 0, 1, {0}},
{"false", "Q.0 Q.1 Q.> . CR", "Should print: 0 (false)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"Q.0=", {
{"true", "Q.0 Q.0= . CR", "Should print: -1 (true)", TEST_NORMAL, 0, 1, {0}},
{"false", "Q.1 Q.0= . CR", "Should print: 0 (false)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"Q.MAX", {
{"basic", "3 Q.FROM-INT 7 Q.FROM-INT Q.MAX Q.TO-INT . CR", "Should print: 7", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.MIN", {
{"basic", "3 Q.FROM-INT 7 Q.FROM-INT Q.MIN Q.TO-INT . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"Q.PRINT", {
{"of_one", "Q.1 Q.PRINT CR", "Should print: 1.00000", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all Q48.16 fixed-point word test suites
* @param vm Pointer to the Forth virtual machine instance
* @details Covers all 23 words registered by register_q48_words()
* (q48_words.c): arithmetic (+, -, *, /), sign (ABS, NEG),
* transcendental approximations (LOG, EXP, SQRT, SIN, COS),
* conversions (FROM-INT, TO-INT), constants (1, 0, SCALE),
* comparisons (=, <, >, 0=, MAX, MIN), and PRINT.
*/
void run_q48_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Q48.16 Words Tests (Module 25)...");
WordContract mod = {CONTRACT_PHYSICS_TRANSPARENT, 0};
for (int i = 0; q48_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite_m(vm, &q48_word_suites[i], mod);
}
print_module_summary("Q48.16 Words", 0, 0, 0, 0);
}
@@ -0,0 +1,104 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Test suites for return stack manipulation words
* @details Contains test cases for >R (push to return stack),
* R> (pop from return stack), and R@ (copy top of return stack)
*/
static WordTestSuite return_stack_word_suites[] = {
{
">R", {
{"basic", "42 >R R@ . R> . CR", "Should print: 42 42", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 >R R@ . R> . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-123 >R R@ . R> . CR", "Should print: -123 -123", TEST_NORMAL, 0, 1, {0}},
{"multiple", "1 2 >R >R R@ . R> . R@ . R> . CR", "Should print: 2 2 1 1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", ">R", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"R>", {
{"basic", "42 >R R> . CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"lifo_order", "1 2 >R >R R> . R> . CR", "Should print: 2 1", TEST_NORMAL, 0, 1, {0}},
{"empty_rstack", "R>", "Should cause return stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"R@", {
{"basic", "42 >R R@ . R> DROP CR", "Should print: 42", TEST_NORMAL, 0, 1, {0}},
{"non_destructive", "99 >R R@ R@ = . R> DROP CR", "Should print: -1", TEST_NORMAL, 0, 1, {0}},
{"empty_rstack", "R@", "Should cause return stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all test suites for return stack manipulation words
* @param vm Pointer to the Forth virtual machine instance
* @details Runs through all defined test cases for >R, R>, and R@ words
* and reports results_run_01_2025_12_08 through the logging system
*/
void run_return_stack_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Return Stack Words Tests (Module 2)...");
WordContract mod = {CONTRACT_PHYSICS_TRANSPARENT, 0};
for (int i = 0; return_stack_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite_m(vm, &return_stack_word_suites[i], mod);
}
print_module_summary("Return Stack Words", 0, 0, 0, 0);
}
@@ -0,0 +1,215 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file stack_words_test.c
* @brief Test module for Forth stack manipulation words
* @details Contains test suites for basic stack operations including
* DUP, DROP, SWAP, OVER, ROT, DEPTH, PICK, and ROLL
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Test suites for stack manipulation words
* @details Contains comprehensive test cases for all standard stack operations,
* including normal cases, edge cases, and error conditions
*/
static WordTestSuite stack_word_suites[] = {
{
"DUP", {
{"basic", "5 DUP . . CR", "Should print: 5 5", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 DUP . . CR", "Should print: 0 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-42 DUP . . CR", "Should print: -42 -42", TEST_NORMAL, 0, 1, {0}},
{"max_int", "2147483647 DUP . . CR", "Should duplicate max int", TEST_EDGE_CASE, 0, 1, {0}},
{"min_int", "-2147483648 DUP . . CR", "Should duplicate min int", TEST_EDGE_CASE, 0, 1, {0}},
{"empty_stack", "DUP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"DROP", {
/* Basic functionality */
{"basic", "5 7 DROP . CR", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"zero", "42 0 DROP . CR", "Should drop zero, print 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "10 -5 DROP . CR", "Should drop negative, print 10", TEST_NORMAL, 0, 1, {0}},
/* Multiple drops */
{"double_drop", "1 2 3 DROP DROP . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{"triple_drop", "1 2 3 4 DROP DROP DROP . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{"sequential", "5 DROP 6 DROP 7 . CR", "Should print: 7", TEST_NORMAL, 0, 1, {0}},
/* Stack depth effects */
{"single_item", "99 DROP DEPTH . CR", "Should empty stack, depth=0", TEST_NORMAL, 0, 1, {0}},
{"depth_change", "DEPTH 1 2 DROP DEPTH SWAP - . CR", "Should change depth by 1", TEST_NORMAL, 0, 1, {0}},
{"preserve_depth", "DEPTH 5 6 DROP DROP DEPTH = . CR", "Should preserve net depth", TEST_NORMAL, 0, 1, {0}},
/* Edge cases */
{"max_int", "2147483647 DROP DEPTH . CR", "Should drop max int", TEST_NORMAL, 0, 1, {0}},
{"min_int", "-2147483648 DROP DEPTH . CR", "Should drop min int", TEST_NORMAL, 0, 1, {0}},
/* Chaining with other operations */
{"with_dup", "5 DUP DROP . CR", "Should duplicate then drop", TEST_NORMAL, 0, 1, {0}},
{"with_swap", "1 2 SWAP DROP . CR", "Should swap then drop", TEST_NORMAL, 0, 1, {0}},
{"complex", "1 2 3 ROT DROP SWAP DROP . CR", "Should handle complex sequence", TEST_NORMAL, 0, 1, {0}},
/* Error case */
{"empty_stack", "DROP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
15, {0}
},
{
"SWAP", {
{"basic", "5 7 SWAP . . CR", "Should print: 7 5", TEST_NORMAL, 0, 1, {0}},
{"same_values", "42 42 SWAP . . CR", "Should print: 42 42", TEST_NORMAL, 0, 1, {0}},
{"zero_nonzero", "0 99 SWAP . . CR", "Should print: 99 0", TEST_NORMAL, 0, 1, {0}},
{"negative", "-5 10 SWAP . . CR", "Should print: 10 -5", TEST_NORMAL, 0, 1, {0}},
{"one_item", "42 SWAP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "SWAP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"OVER", {
{"basic", "5 7 OVER . . . CR", "Should print: 5 7 5", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 OVER . . . CR", "Should print: 0 0 0", TEST_NORMAL, 0, 1, {0}},
{"mixed", "-1 42 OVER . . . CR", "Should print: -1 42 -1", TEST_NORMAL, 0, 1, {0}},
{"one_item", "42 OVER", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "OVER", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"ROT", {
{"basic", "1 2 3 ROT . . . CR", "Should print: 2 3 1", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 0 ROT . . . CR", "Should print: 0 0 0", TEST_NORMAL, 0, 1, {0}},
{"mixed", "-1 0 1 ROT . . . CR", "Should print: 0 1 -1", TEST_NORMAL, 0, 1, {0}},
{"two_items", "1 2 ROT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"one_item", "42 ROT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "ROT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"DEPTH", {
{"empty", "DEPTH . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"one_item", "42 DEPTH . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{"multiple", "1 2 3 DEPTH . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"PICK", {
{"pick_0", "1 2 3 0 PICK . CR", "Should print: 3", TEST_NORMAL, 0, 1, {0}},
{"pick_1", "1 2 3 1 PICK . CR", "Should print: 2", TEST_NORMAL, 0, 1, {0}},
{"pick_2", "1 2 3 2 PICK . CR", "Should print: 1", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"ROLL", {
{"roll_1", "1 2 3 1 ROLL . . . CR", "Should print: 1 3 2", TEST_NORMAL, 0, 1, {0}},
{"roll_2", "1 2 3 2 ROLL . . . CR", "Should print: 2 3 1", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"?DUP", {
{"nonzero", "42 ?DUP . . CR", "Should print: 42 42", TEST_NORMAL, 0, 1, {0}},
{"negative", "-5 ?DUP . . CR", "Should print: -5 -5", TEST_NORMAL, 0, 1, {0}},
{"zero", "0 ?DUP DEPTH . CR", "Should not dup: depth 1", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "?DUP", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"-ROT", {
{"basic", "1 2 3 -ROT . . . CR", "Should print: 1 3 2", TEST_NORMAL, 0, 1, {0}},
{"zeros", "0 0 0 -ROT . . . CR", "Should print: 0 0 0", TEST_NORMAL, 0, 1, {0}},
{"roundtrip", "1 2 3 -ROT ROT . . . CR", "Should restore original order", TEST_NORMAL, 0, 1, {0}},
{"two_items", "1 2 -ROT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "-ROT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all test suites for stack manipulation words
* @param vm Pointer to the Forth virtual machine instance
* @details Runs through all defined test cases for stack manipulation words
* and reports results_run_01_2025_12_08 through the logging system
*/
void run_stack_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Stack Words Tests (Module 1: Foundation)...");
WordContract mod = {CONTRACT_PHYSICS_TRANSPARENT, 0};
for (int i = 0; stack_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite_m(vm, &stack_word_suites[i], mod);
}
print_module_summary("Stack Words", 0, 0, 0, 0); /* Will be calculated by test_suite */
}
@@ -0,0 +1,222 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Test suites for StarForth-specific words
*
* Contains test cases for the following StarForth implementation words:
* - ENTROPY@: Fetch word execution count
* - ENTROPY!: Store word execution count
* - WORD-ENTROPY: Get execution count for a word by name
* - RESET-ENTROPY: Reset all word execution counts
* - TOP-WORDS: Display most frequently executed words
* - (-: Comment to end of line
* - INIT: System initialization
*/
static WordTestSuite starforth_word_suites[] = {
{
"ENTROPY@", {
/* Basic functionality - ' returns xt (DictEntry*), NOT body address */
{
"after_execution", ": TESTWORD 1 1 + DROP ; TESTWORD ' TESTWORD ENTROPY@ . CR",
"Should show execution count > 0", TEST_NORMAL, 0, 1, {0}
},
{
"zero_initial", ": NEWWORD ; ' NEWWORD ENTROPY@ . CR",
"Should show 0 for unused word", TEST_NORMAL, 0, 1, {0}
},
/* Multiple executions */
{
"multiple_calls", ": ENTTEST 1 DROP ; ENTTEST ENTTEST ENTTEST ' ENTTEST ENTROPY@ . CR",
"Should increment with each call", TEST_NORMAL, 0, 1, {0}
},
/* Error cases */
{"null_addr", "0 ENTROPY@", "Should handle null address", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "ENTROPY@", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"ENTROPY!", {
/* Basic functionality - ' returns xt (DictEntry*), NOT body address */
{
"set_entropy", ": ENTSET ; 42 ' ENTSET ENTROPY! ' ENTSET ENTROPY@ . CR",
"Should set entropy to 42", TEST_NORMAL, 0, 1, {0}
},
{
"overwrite",
": ENTOV ; 10 ' ENTOV ENTROPY! 20 ' ENTOV ENTROPY! ' ENTOV ENTROPY@ . CR",
"Should overwrite previous value", TEST_NORMAL, 0, 1, {0}
},
{
"zero_entropy", ": ENTZERO ; 0 ' ENTZERO ENTROPY! ' ENTZERO ENTROPY@ . CR",
"Should set entropy to 0", TEST_NORMAL, 0, 1, {0}
},
/* Error cases */
{"null_addr", "42 0 ENTROPY!", "Should handle null address", TEST_ERROR_CASE, 1, 1, {0}},
{"one_arg", "42 ENTROPY!", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{"empty_stack", "ENTROPY!", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"WORD-ENTROPY", {
/* Basic functionality - WORD-ENTROPY just prints stats, no stack interaction */
{
"display_all", "1 DUP DUP DUP DROP DROP DROP DROP WORD-ENTROPY",
"Should display all word entropy stats", TEST_NORMAL, 0, 1, {0}
},
{
"after_execution", ": WDENT 1 2 + DROP ; WDENT WDENT WORD-ENTROPY",
"Should show defined word entropy", TEST_NORMAL, 0, 1, {0}
},
/* No stack effect */
{
"stack_preserved", "DEPTH WORD-ENTROPY DEPTH = . CR",
"Should not affect stack", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"RESET-ENTROPY", {
/* Basic functionality */
{
"reset_defined", ": RESETTEST 1 DROP ; RESETTEST RESETTEST RESETTEST RESET-ENTROPY",
"Should reset all entropy counters", TEST_NORMAL, 0, 1, {0}
},
/* Idempotent */
{"double_reset", "RESET-ENTROPY RESET-ENTROPY", "Should handle multiple resets", TEST_NORMAL, 0, 1, {0}},
/* Verify it runs without error */
{
"after_usage", "1 DUP DUP DUP DROP DROP DROP DROP RESET-ENTROPY", "Should reset after word usage",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"TOP-WORDS", {
/* Basic functionality - TOP-WORDS requires ( n -- ) stack parameter */
{
"display_top5", ": TOPTEST 1 DROP ; TOPTEST TOPTEST TOPTEST TOPTEST TOPTEST 5 TOP-WORDS",
"Should display top 5 hot words", TEST_NORMAL, 0, 1, {0}
},
{"display_top10", "10 TOP-WORDS", "Should display top 10 words", TEST_NORMAL, 0, 1, {0}},
{"after_reset", "RESET-ENTROPY 5 TOP-WORDS", "Should handle display after reset", TEST_NORMAL, 0, 1, {0}},
/* Different counts */
{"single_word", "1 TOP-WORDS", "Should display top 1 word", TEST_NORMAL, 0, 1, {0}},
{"large_count", "100 TOP-WORDS", "Should handle large count", TEST_NORMAL, 0, 1, {0}},
/* Error cases */
{"zero_count", "0 TOP-WORDS", "Should handle zero count", TEST_NORMAL, 0, 1, {0}},
{"negative_count", "-5 TOP-WORDS", "Should handle negative count", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "TOP-WORDS", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
8, {0}
},
{
"(-", {
/* Basic functionality */
{"comment_line", "(- This is a comment\n1 2 + . CR", "Should ignore rest of line", TEST_NORMAL, 0, 1, {0}},
{"mid_line", "1 (- comment here\n2 + . CR", "Should work mid-expression", TEST_NORMAL, 0, 1, {0}},
/* Stack preservation */
{"no_stack_effect", "DEPTH (- comment\nDEPTH = . CR", "Should not affect stack", TEST_NORMAL, 0, 1, {0}},
/* Multiple comments */
{"multiple", "1 (- first\n2 (- second\n+ . CR", "Should handle multiple comments", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
/* INIT intentionally not tested here - it's a startup operation that loads
* and executes init.4th. Running it during POST would cause double-initialization. */
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all StarForth words test suites
* @param vm Pointer to the FORTH virtual machine instance
*
* Runs through all test cases for StarForth-specific implementation words,
* including entropy tracking, comments, and initialization.
*/
void run_starforth_words_tests(VM *vm) {
log_message(LOG_INFO, "Running StarForth Words Tests (Module 17: StarForth Extensions)...");
for (int i = 0; starforth_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &starforth_word_suites[i]);
}
print_module_summary("StarForth Words", 0, 0, 0, 0);
}
+365
View File
@@ -0,0 +1,365 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file stress_tests.c
* @brief FORTH-79 Standard Comprehensive Stress Testing Module
*
* This module contains extensive stress tests that verify the interpreter's behavior
* under extreme conditions while remaining within FORTH-79 standard compliance.
*
* Test categories (expanded):
* - Deep nesting of control structures (up to 5 levels)
* - Stack exhaustion and boundary tests (near-limit operations)
* - Numeric boundary conditions (cell_t min/max, overflow)
* - Dictionary pressure (many definitions, long names)
* - Word execution chains (deep call stacks)
* - Control flow edge cases (empty loops, large counts, EXIT)
* - Memory allocation patterns (fragmentation, large blocks)
* - Error recovery (ABORT, stack underflow recovery)
* - Large definitions and word chains
*
* IMPORTANT: These tests only use FORTH-79 standard words. Features from
* later standards (ANS Forth 1994, Forth 2012) are NOT tested here:
* - RECURSE (ANS Forth) - not in FORTH-79
* - Self-referential definitions - not supported in FORTH-79
* - Extended VARIABLE usage - only standard semantics tested
*
* For testing extended/non-standard features, see integration_tests.c
*/
#include <stdio.h>
#include "test_common.h"
#include "test_runner.h"
/* Comprehensive stress test suites */
static WordTestSuite stress_suites[] = {
/* ===== 1. STACK DEPTH TESTS ===== */
{
"STACK_DEPTH", {
{"deep_push", ": PUSH-100 0 100 0 DO I LOOP ; PUSH-100 DEPTH . CR", "Push 100 items", TEST_NORMAL, 0, 1, {0}},
{"deep_math", ": MATH-STRESS 1 100 0 DO 1+ LOOP . CR ; MATH-STRESS", "100 additions", TEST_NORMAL, 0, 1, {0}},
{
"dup_stress", ": DUP-STRESS 42 50 0 DO DUP LOOP DEPTH . CR 51 0 DO DROP LOOP ; DUP-STRESS", "50 DUPs",
TEST_NORMAL, 0, 1, {0}
},
{
"deep_push_200",
": PUSH-200 0 200 0 DO I LOOP ; : CLR-200 200 0 DO DROP LOOP ; PUSH-200 DEPTH . CR CLR-200",
"Push 200 items", TEST_NORMAL, 0, 1, {0}
},
{
"mixed_ops", ": MIXED 1 2 3 4 5 6 7 8 9 10 50 0 DO SWAP ROT LOOP 10 0 DO DROP LOOP ; MIXED",
"Mixed stack ops", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
/* ===== 2. STACK BOUNDARY TESTS ===== */
{
"STACK_BOUNDARY", {
{
"near_limit_900",
": PUSH-900 0 900 0 DO I LOOP ; : CLR-900 900 0 DO DROP LOOP ; PUSH-900 DEPTH . CR CLR-900 FORGET CLR-900",
"Push 900 items (near 1024 limit)", TEST_EDGE_CASE, 0, 1, {0}
},
{"underflow_detect", "DROP", "Stack underflow detection", TEST_ERROR_CASE, 1, 1, {0}},
{"dup_underflow", "DUP", "DUP underflow detection", TEST_ERROR_CASE, 1, 1, {0}},
{"swap_underflow", "1 SWAP", "SWAP underflow detection", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
/* ===== 3. RETURN STACK STRESS ===== */
{
"RETURN_STACK", {
{"basic_to_r", "1 2 3 >R >R >R R> R> R> + + . CR", "Basic >R/R> test", TEST_NORMAL, 0, 1, {0}},
{"r_fetch", "42 >R R@ . CR R> DROP", "R@ test", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* ===== 4. DEEP NESTING TESTS ===== */
{
"DEEP_NESTING", {
{
"nested_if_5",
": NEST5 DUP 0> IF DUP 10 > IF DUP 20 > IF DUP 30 > IF DUP 40 > IF 5 ELSE 4 THEN ELSE 3 THEN ELSE 2 THEN ELSE 1 THEN ELSE 0 THEN . CR ; 45 NEST5",
"5-level IF nesting", TEST_NORMAL, 0, 1, {0}
},
{
"nested_loops_4", ": NEST-4 0 2 0 DO 2 0 DO 2 0 DO 2 0 DO 1+ LOOP LOOP LOOP LOOP . CR ; NEST-4",
"4-level DO loop nesting", TEST_NORMAL, 0, 1, {0}
},
{
"mixed_if_do", ": MIX-NEST 0 10 0> IF 5 0 DO 1+ LOOP ELSE 3 0 DO 2 + LOOP THEN . CR ; MIX-NEST",
"IF with DO inside", TEST_NORMAL, 0, 1, {0}
},
{
"begin_simple", ": BEGIN-TEST 5 BEGIN 1- DUP 0= UNTIL DROP ; BEGIN-TEST",
"Simple BEGIN/UNTIL loop", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
/* ===== 5. CONTROL FLOW EDGE CASES ===== */
{
"CONTROL_EDGE", {
{"empty_loop", ": EMPTY-LOOP 0 0 DO LOOP ; EMPTY-LOOP", "Empty loop body", TEST_NORMAL, 0, 1, {0}},
{"single_iter", ": SINGLE 1 0 DO 42 . LOOP CR ; SINGLE", "Single iteration", TEST_NORMAL, 0, 1, {0}},
{"large_count", ": BIG-LOOP 0 10000 0 DO 1+ LOOP . CR ; BIG-LOOP", "10000 iterations", TEST_NORMAL, 0, 1, {0}},
{
"nested_exit", ": NEST-EXIT 10 0 DO I 5 = IF EXIT THEN I . LOOP CR ; NEST-EXIT", "EXIT in nested loop",
TEST_NORMAL, 0, 1, {0}
},
{
"begin_while", ": B-W-R 10 BEGIN DUP 0> WHILE DUP . 1- REPEAT DROP CR ; B-W-R", "BEGIN WHILE REPEAT",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
/* ===== 6. NUMERIC BOUNDARIES ===== */
{
"NUMERIC_BOUNDARY", {
{"max_int", ": MAX-TEST 2147483647 DUP . 1+ . CR ; MAX-TEST", "Max int boundary", TEST_EDGE_CASE, 0, 1, {0}},
{"min_int", ": MIN-TEST -2147483648 DUP . 1- . CR ; MIN-TEST", "Min int boundary", TEST_EDGE_CASE, 0, 1, {0}},
{"large_mult", ": BIG-MULT 10000 10000 * . CR ; BIG-MULT", "Large multiplication", TEST_NORMAL, 0, 1, {0}},
{
"div_edge", ": DIV-TEST 100 1 / 100 2 / 100 3 / . . . CR ; DIV-TEST", "Division edge cases",
TEST_NORMAL, 0, 1, {0}
},
{"mod_edge", ": MOD-TEST 100 7 MOD 100 3 MOD . . CR ; MOD-TEST", "Modulo operations", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
/* ===== 7. WORD EXECUTION CHAINS ===== */
{
"EXEC_CHAINS", {
{
"chain_10",
": C1 1 ; : C2 C1 1+ ; : C3 C2 1+ ; : C4 C3 1+ ; : C5 C4 1+ ; : C6 C5 1+ ; : C7 C6 1+ ; : C8 C7 1+ ; : C9 C8 1+ ; : C10 C9 1+ ; C10 . CR",
"10-word call chain", TEST_NORMAL, 0, 1, {0}
},
{
"chain_20",
": D1 1 ; : D2 D1 1+ ; : D3 D2 1+ ; : D4 D3 1+ ; : D5 D4 1+ ; : D6 D5 1+ ; : D7 D6 1+ ; : D8 D7 1+ ; : D9 D8 1+ ; : D10 D9 1+ ; : D11 D10 1+ ; : D12 D11 1+ ; : D13 D12 1+ ; : D14 D13 1+ ; : D15 D14 1+ ; : D16 D15 1+ ; : D17 D16 1+ ; : D18 D17 1+ ; : D19 D18 1+ ; : D20 D19 1+ ; D20 . CR",
"20-word call chain", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* ===== 8. LARGE DEFINITIONS ===== */
{
"LARGE_DEF", {
{
"long_sequence_50",
": LONG50 1 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ 1+ . CR ; LONG50",
"50+ operations", TEST_NORMAL, 0, 1, {0}
},
{
"many_literals",
": LITS 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + + + + + + + + + + + + + + + + + + + . CR ; LITS",
"20 literals", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* ===== 9. DICTIONARY PRESSURE ===== */
{
"DICT_PRESSURE", {
{
"many_defs",
": T1 1 ; : T2 2 ; : T3 3 ; : T4 4 ; : T5 5 ; : T6 6 ; : T7 7 ; : T8 8 ; : T9 9 ; : T10 10 ; : T11 11 ; : T12 12 ; : T13 13 ; : T14 14 ; : T15 15 ; : T16 16 ; : T17 17 ; : T18 18 ; : T19 19 ; : T20 20 ; T20 . CR",
"20 word definitions", TEST_NORMAL, 0, 1, {0}
},
{
"long_name", ": VERY-LONG-WORD-NAME-TEST-MAX 42 . CR ; VERY-LONG-WORD-NAME-TEST-MAX",
"Long word name (31 chars)", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* ===== 10. MEMORY ALLOCATION STRESS ===== */
{
"MEMORY_STRESS", {
{
"allot_100", ": ALLOT-100 HERE 100 ALLOT HERE SWAP - . CR ; ALLOT-100", "ALLOT 100 bytes", TEST_NORMAL, 0, 1, {0}
},
{"allot_1000", ": ALLOT-1K HERE 1000 ALLOT HERE SWAP - . CR ; ALLOT-1K", "ALLOT 1KB", TEST_NORMAL, 0, 1, {0}},
{"allot_8k", ": ALLOT-8K HERE 8192 ALLOT HERE SWAP - . CR ; ALLOT-8K", "ALLOT 8KB", TEST_NORMAL, 0, 1, {0}},
{
"multiple_allots", ": MULTI-ALLOT HERE 10 ALLOT 20 ALLOT 30 ALLOT HERE SWAP - . CR ; MULTI-ALLOT",
"Multiple ALLOTs", TEST_NORMAL, 0, 1, {0}
},
{
"allot_pattern", ": PATTERN 5 0 DO HERE 100 ALLOT DROP LOOP ; PATTERN", "Repeated allot pattern",
TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
/* ===== 11. ERROR RECOVERY ===== */
{
"ERROR_RECOVERY", {
{"abort_recovery", ": AB-TEST 1 2 3 ABORT ; AB-TEST DEPTH . CR", "ABORT clears stacks", TEST_NORMAL, 0, 1, {0}},
{"continue_after_abort", "ABORT 42 . CR", "Can continue after ABORT", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* ===== 12. DOUBLE NUMBER STRESS ===== */
{
"DOUBLE_STRESS", {
{"large_double_add", "1000000 0 2000000 0 D+ . . CR", "Large double addition", TEST_NORMAL, 0, 1, {0}},
{"double_chain", "100 200 2DUP D+ . . CR", "Double operation chain", TEST_NORMAL, 0, 1, {0}},
{"double_negate", "100 200 DNEGATE DNEGATE . . CR", "Double negate twice", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
/* ===== 13. MIXED COMPLEXITY ===== */
{
"MIXED_COMPLEX", {
{
"nested_if_do",
": COMPLEX 10 0 DO I DUP 5 > IF 2 * ELSE 3 + THEN . LOOP CR ; COMPLEX",
"Mixed IF/DO", TEST_NORMAL, 0, 1, {0}
},
{
"nested_begin_do",
": NB-DO 5 BEGIN DUP 0> WHILE DUP 3 0 DO I . LOOP CR 1- REPEAT DROP ; NB-DO",
"Nested BEGIN/DO", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* ===== 14. STRING/BUFFER EDGE CASES ===== */
{
"STRING_EDGE", {
{"empty_def", ": EMPTY ; EMPTY", "Empty definition", TEST_NORMAL, 0, 1, {0}},
{"single_word", ": SINGLE DUP ; 42 SINGLE . . CR", "Single word def", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
};
/**
* @brief Execute comprehensive stress test suites
*
* Runs an extensive set of stress tests to verify the VM's behavior
* under extreme conditions. Tests include:
* - Stack operations and boundaries
* - Deep nesting and control flow
* - Numeric boundaries and overflow
* - Dictionary pressure
* - Memory allocation patterns
* - Error recovery
* - Mixed complexity scenarios
*
* All tests are FORTH-79 compliant.
*
* @param vm Pointer to the Forth virtual machine instance
*/
void run_stress_tests(VM *vm) {
if (!vm) return;
// Reset global stats for stress tests
extern TestStats global_test_stats;
global_test_stats.total_tests = 0;
global_test_stats.total_pass = 0;
global_test_stats.total_fail = 0;
global_test_stats.total_skip = 0;
global_test_stats.total_error = 0;
log_message(LOG_INFO, "==============================================");
log_message(LOG_INFO, " StarForth Comprehensive Stress Test Suite");
log_message(LOG_INFO, " FORTH-79 Standard Compliance");
log_message(LOG_INFO, "==============================================\n");
for (size_t i = 0; i < sizeof(stress_suites) / sizeof(WordTestSuite); i++) {
log_message(LOG_TEST, "▶ Testing category: %s", stress_suites[i].word_name);
run_test_suite(vm, &stress_suites[i]);
}
log_message(LOG_INFO, "==============================================");
log_message(LOG_INFO, " COMPREHENSIVE STRESS TEST RESULTS");
log_message(LOG_INFO, "==============================================");
log_message(LOG_INFO, " Total tests: %d", global_test_stats.total_tests);
log_message(LOG_INFO, " Passed: %d", global_test_stats.total_pass);
log_message(LOG_INFO, " Failed: %d", global_test_stats.total_fail);
log_message(LOG_INFO, " Skipped: %d", global_test_stats.total_skip);
log_message(LOG_INFO, " Errors: %d", global_test_stats.total_error);
if (global_test_stats.total_fail == 0 && global_test_stats.total_error == 0) {
log_message(LOG_INFO, "\n ✓ ALL STRESS TESTS PASSED!");
log_message(LOG_INFO, " StarForth demonstrates excellent stability");
log_message(LOG_INFO, " under extreme FORTH-79 workload conditions.");
} else {
log_message(LOG_INFO, "\n ✗ SOME TESTS FAILED");
log_message(LOG_INFO, " Review output above for details");
}
log_message(LOG_INFO, "==============================================\n");
}
@@ -0,0 +1,219 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/* String Words Test Suites - Module 11 */
static WordTestSuite string_word_suites[] = {
{
"COUNT", {
{"basic", "HERE S\" Test\" DROP COUNT . . CR", "Should show length and addr+1", TEST_NORMAL, 0, 1, {0}},
{"empty", "HERE 0 OVER C! COUNT . . CR", "Should handle empty string", TEST_NORMAL, 0, 1, {0}},
{"max_length", "HERE 255 OVER C! COUNT . . CR", "Should handle max length", TEST_NORMAL, 0, 1, {0}},
{"string_bounds", "HERE COUNT SWAP 1- = . CR", "Should increment address", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "COUNT", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"-TRAILING", {
{"basic", "HERE S\" Test \" -TRAILING TYPE CR", "Should trim spaces", TEST_NORMAL, 0, 1, {0}},
{"all_spaces", "HERE S\" \" -TRAILING TYPE CR", "Should handle all spaces", TEST_NORMAL, 0, 1, {0}},
{"no_spaces", "HERE S\" Test\" -TRAILING TYPE CR", "Should handle no spaces", TEST_NORMAL, 0, 1, {0}},
{"empty", "HERE 0 -TRAILING TYPE CR", "Should handle empty string", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"CMOVE", {
{"basic", "HERE S\" Test\" DUP >R HERE 10 + SWAP CMOVE CR", "Should copy string", TEST_NORMAL, 0, 1, {0}},
{"empty", "HERE HERE 10 + 0 CMOVE", "Should handle zero count", TEST_NORMAL, 0, 1, {0}},
{"overlap", "HERE DUP 1+ 5 CMOVE", "Should handle overlap", TEST_NORMAL, 0, 1, {0}},
{"bounds", "HERE PAD 1000 CMOVE", "Should check bounds", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"CMOVE>", {
{"basic", "HERE S\" Test\" DUP >R HERE 10 + SWAP CMOVE> CR", "Should copy backward", TEST_NORMAL, 0, 1, {0}},
{"empty", "HERE HERE 10 + 0 CMOVE>", "Should handle zero count", TEST_NORMAL, 0, 1, {0}},
{"overlap", "HERE DUP 1+ 5 CMOVE>", "Should handle overlap", TEST_NORMAL, 0, 1, {0}},
{"bounds", "HERE PAD 1000 CMOVE>", "Should check bounds", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"COMPARE", {
{"equal", "HERE S\" Test\" HERE S\" Test\" COMPARE . CR", "Should return 0", TEST_NORMAL, 0, 1, {0}},
{"less", "HERE S\" Test1\" HERE S\" Test2\" COMPARE . CR", "Should return -1", TEST_NORMAL, 0, 1, {0}},
{"greater", "HERE S\" Test2\" HERE S\" Test1\" COMPARE . CR", "Should return 1", TEST_NORMAL, 0, 1, {0}},
{
"different_lengths", "HERE S\" Test\" HERE S\" Testing\" COMPARE . CR", "Should handle lengths",
TEST_NORMAL, 0, 1, {0}
},
{"empty", "HERE 0 HERE 0 COMPARE . CR", "Should handle empty", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
5, {0}
},
{
"SEARCH", {
{"found", "HERE S\" Testing\" HERE S\" Test\" SEARCH . . . CR", "Should find substring", TEST_NORMAL, 0, 1, {0}},
{
"not_found", "HERE S\" Testing\" HERE S\" Xyz\" SEARCH . . . CR", "Should return false", TEST_NORMAL, 0,
1, {0}
},
{
"empty_pattern", "HERE S\" Test\" HERE 0 SEARCH . . . CR", "Should handle empty pattern", TEST_NORMAL, 0, 1, {0}
},
{"empty_string", "HERE 0 HERE S\" Test\" SEARCH . . . CR", "Should handle empty string", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"SCAN", {
{"basic", "HERE S\" Test,Data\" BL SCAN TYPE CR", "Should find space", TEST_NORMAL, 0, 1, {0}},
{"not_found", "HERE S\" TestData\" BL SCAN TYPE CR", "Should scan to end", TEST_NORMAL, 0, 1, {0}},
{"empty", "HERE 0 BL SCAN TYPE CR", "Should handle empty", TEST_NORMAL, 0, 1, {0}},
{"all_delims", "HERE S\" \" BL SCAN TYPE CR", "Should handle all delims", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"SKIP", {
{"basic", "HERE S\" Test\" BL SKIP TYPE CR", "Should skip spaces", TEST_NORMAL, 0, 1, {0}},
{"no_delims", "HERE S\" Test\" BL SKIP TYPE CR", "Should not skip", TEST_NORMAL, 0, 1, {0}},
{"empty", "HERE 0 BL SKIP TYPE CR", "Should handle empty", TEST_NORMAL, 0, 1, {0}},
{"all_delims", "HERE S\" \" BL SKIP TYPE CR", "Should skip all", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
4, {0}
},
{
"BLANK", {
{"basic", "HERE 5 BLANK HERE 5 TYPE CR", "Should fill with spaces", TEST_NORMAL, 0, 1, {0}},
{"zero", "HERE 0 BLANK HERE 0 TYPE CR", "Should handle zero count", TEST_NORMAL, 0, 1, {0}},
/* TODO: Bounds checking - memory subsystem doesn't yet enforce upper bounds */
/* {"bounds", "HERE 2000000 BLANK", "Should check bounds", TEST_ERROR_CASE, 1, 1}, */
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"S\"", {
{"interpret_len", "S\" hello\" . CR", "Should print: 5 (length)", TEST_NORMAL, 0, 1, {0}},
{"interpret_type", "S\" hello\" TYPE CR", "Should print: hello", TEST_NORMAL, 0, 1, {0}},
{"empty", "S\" \" . CR", "Should print: 0", TEST_NORMAL, 0, 1, {0}},
{"compile_type", ": TSQ S\" world\" TYPE CR ; TSQ", "Should print: world", TEST_NORMAL, 0, 1, {0}},
{"compile_len", ": TSQ2 S\" forth\" SWAP DROP . CR ; TSQ2", "Should print: 5", TEST_NORMAL, 0, 1, {0}},
{"compile_twice", ": TSQ3 S\" ab\" TYPE S\" cd\" TYPE CR ; TSQ3", "Should print: abcd", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
6, {0}
},
{
"BL", {
{"value", "BL . CR", "Should print: 32", TEST_NORMAL, 0, 1, {0}},
{"as_char", "BL EMIT CR", "Should emit a space", TEST_NORMAL, 0, 1, {0}},
{"in_scan", "HERE S\" a b\" BL SCAN SWAP DROP . CR", "Should find space at offset 1", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"SOURCE", {
{"returns_pair", "SOURCE SWAP DROP 0< NOT . CR", "Should return non-negative length", TEST_NORMAL, 0, 1, {0}},
{"has_addr", "SOURCE DROP 0<> . CR", "Should return non-null address", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
">IN", {
{"initial", ">IN @ . CR", "Should return input offset", TEST_NORMAL, 0, 1, {0}},
{"advances", ": TWIN >IN @ >IN @ - . CR ; TWIN", "Should print: 0 (no advance inside compiled word)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all string manipulation word tests
*
* @details Runs test suites for FORTH string operations including COUNT, -TRAILING,
* CMOVE, CMOVE>, COMPARE, SEARCH, SCAN, SKIP, and BLANK
*
* @param vm Pointer to the Forth virtual machine instance
*/
void run_string_words_tests(VM *vm) {
log_message(LOG_INFO, "Running String Words Tests (Module 11)...");
for (int i = 0; string_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &string_word_suites[i]);
}
print_module_summary("String Words", 0, 0, 0, 0);
}
@@ -0,0 +1,133 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/**
* @brief Test suite definitions for system control words
* @details Contains test cases for each system word including normal operations
* and error conditions
*/
static WordTestSuite system_word_suites[] = {
{
"QUIT", {
{"basic", "QUIT", "Should reset stacks and state", TEST_NORMAL, 0, 0, {0}}, // Interactive
{"in_definition", ": BAD-WORD QUIT ;", "Should prevent compilation", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
2, {0}
},
{
"ABORT", {
{"basic", "ABORT", "Should clear stacks and return to QUIT", TEST_NORMAL, 0, 1, {0}},
{"with_data", "1 2 3 ABORT DEPTH . CR", "Should clear stack", TEST_NORMAL, 0, 1, {0}},
{
"in_definition_runtime", ": BAD-WORD ABORT ; 123 BAD-WORD DEPTH . CR",
"ABORT may appear in a definition; when executed it clears both stacks", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"ABORT\"", {
{
"condition_true", "-1 ABORT\" Error\"", "Should abort with message (no error; stacks cleared to QUIT)",
TEST_NORMAL, 0, 1, {0}
},
{"condition_false", "0 ABORT\" Error\"", "Should not abort", TEST_NORMAL, 0, 1, {0}},
{"empty_stack", "ABORT\"", "Should cause stack underflow", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"BYE", {
{"basic", "BYE", "Should exit cleanly", TEST_NORMAL, 0, 0, {0}}, // Can't really test
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"COLD", {
{"basic", "COLD", "Should reset system", TEST_NORMAL, 0, 0, {0}}, // Dangerous to test
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
{
"WARM", {
{"basic", "WARM", "Should soft reset", TEST_NORMAL, 0, 0, {0}}, // Dangerous to test
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
1, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/**
* @brief Executes all system word tests
* @details Runs test suites for FORTH system control operations including QUIT,
* ABORT, ABORT", BYE, COLD, and WARM. Some tests are marked as unimplemented
* due to their system-level effects
*
* @param vm Pointer to the Forth virtual machine instance
*/
void run_system_words_tests(VM *vm) {
log_message(LOG_INFO, "Running System Words Tests (Module 18)...");
log_message(LOG_WARN, "Some system tests marked as unimplemented due to system-level effects");
for (int i = 0; system_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &system_word_suites[i]);
}
print_module_summary("System Words", 0, 0, 0, 0);
}
@@ -0,0 +1,163 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/test_runner.h"
#include "../include/test_common.h"
/** @brief Test suites for vocabulary-related FORTH words (Module 15)
* @details Contains test cases for each vocabulary word, including normal operations
* and error cases. Each suite tests specific functionality of the vocabulary system.
*/
static WordTestSuite vocabulary_word_suites[] = {
{
"VOCABULARY", {
/* Basic functionality */
{"basic", "VOCABULARY TEST-VOC1 TEST-VOC1 DEFINITIONS", "Should create vocabulary", TEST_NORMAL, 0, 1, {0}},
{
"create_and_switch", "VOCABULARY MYVOC MYVOC DEFINITIONS CONTEXT @ . CR", "Should create and switch",
TEST_NORMAL, 0, 1, {0}
},
/* Word isolation */
{
"word_isolation", "VOCABULARY ISOLATED ISOLATED DEFINITIONS : ISOWORD 99 ; FORTH DEFINITIONS",
"Should create word in isolated vocab", TEST_NORMAL, 0, 1, {0}
},
{
"cross_vocab_access", "VOCABULARY V1 V1 DEFINITIONS : V1WORD 11 ; FORTH V1WORD . CR",
"Should access word across vocabs", TEST_NORMAL, 0, 1, {0}
},
/* Multiple vocabularies */
{
"multiple_vocabs", "VOCABULARY VA VOCABULARY VB VOCABULARY VC VA DEFINITIONS",
"Should create multiple vocabs", TEST_NORMAL, 0, 1, {0}
},
/* Error cases */
{
"duplicate", "VOCABULARY TESTVOC VOCABULARY TESTVOC", "Should handle duplicate", TEST_ERROR_CASE, 1,
1, {0}
},
{"empty_name", "VOCABULARY", "Should handle empty name", TEST_ERROR_CASE, 1, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
7, {0}
},
{
"FORTH", {
{"basic", "FORTH DEFINITIONS", "Should select FORTH vocab", TEST_NORMAL, 0, 1, {0}},
{"persistence", "FORTH : TEST1 42 ; TEST1 . CR", "Should find word in FORTH", TEST_NORMAL, 0, 1, {0}},
{"from_other", "VOCABULARY OTHER-VOC OTHER-VOC FORTH", "Should return to FORTH", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"DEFINITIONS", {
{"basic", "FORTH DEFINITIONS", "Should set current vocabulary", TEST_NORMAL, 0, 1, {0}},
{"new_vocab", "VOCABULARY TEST-VOC6 TEST-VOC6 DEFINITIONS", "Should set new current", TEST_NORMAL, 0, 1, {0}},
{
"word_creation", "VOCABULARY TEST-VOC7 TEST-VOC7 DEFINITIONS : TEST2 43 ;",
"Should create in correct vocab", TEST_NORMAL, 0, 1, {0}
},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"CONTEXT", {
{"basic", "CONTEXT @ . CR", "Should show current context", TEST_NORMAL, 0, 1, {0}},
{"modify", "CONTEXT @ 1+ CONTEXT !", "Should allow modification (impl-defined effect)", TEST_NORMAL, 0, 1, {0}},
{"initial", "FORTH CONTEXT @ . CR", "Should show FORTH vocab", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"CURRENT", {
{"basic", "CURRENT @ . CR", "Should show current vocab", TEST_NORMAL, 0, 1, {0}},
{
"after_def", "VOCABULARY TEST-VOC8 TEST-VOC8 DEFINITIONS CURRENT @ . CR", "Should show new vocab",
TEST_NORMAL, 0, 1, {0}
},
{"protect", "CURRENT @ 1+ CURRENT !", "Should allow modification (impl-defined effect)", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
{
"ORDER", {
{"basic", "ORDER", "Should list search order", TEST_NORMAL, 0, 1, {0}},
{
"multiple", "VOCABULARY TEST-VOC9 TEST-VOC9 DEFINITIONS ORDER", "Should show all vocabs", TEST_NORMAL, 0, 1, {0}
},
{"after_forth", "TEST-VOC9 FORTH ORDER", "Should show FORTH only", TEST_NORMAL, 0, 1, {0}},
{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}
},
3, {0}
},
/* End marker */
{NULL, {{NULL, NULL, NULL, TEST_NORMAL, 0, 0, {0}}}, 0, {0}}
};
/** @brief Executes all vocabulary word test suites
* @param vm Pointer to the Forth virtual machine instance
* @details Runs through all vocabulary-related test suites, executing each test case
* and logging the results_run_01_2025_12_08. Tests vocabulary creation, selection, and manipulation.
*/
void run_vocabulary_words_tests(VM *vm) {
log_message(LOG_INFO, "Running Vocabulary Words Tests (Module 15)...");
for (int i = 0; vocabulary_word_suites[i].word_name != NULL; i++) {
log_message(LOG_TEST, "▶ Testing module: %s", __FILE__);
run_test_suite(vm, &vocabulary_word_suites[i]);
}
print_module_summary("Vocabulary Words", 0, 0, 0, 0);
}
+510
View File
@@ -0,0 +1,510 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../../include/vm.h"
#include "../../include/vm_debug.h"
#include "include/test_common.h"
#include "include/test_contracts.h"
#include "../../include/log.h"
#include "test_runner.h"
#ifdef __STARKERNEL__
#include "starkernel/hal/hal.h"
#define test_fatal_exit() sk_hal_panic("test failure - fail fast")
#else
#include <stdio.h>
#include <stdlib.h>
#define test_fatal_exit() exit(1)
#endif
/**
* @brief Global flag to control test execution behavior
* @details When set to non-zero, causes test runner to exit immediately on first failure
*/
int fail_fast = 0;
/* ---------- debug helpers (dump on fatal and exit) ---------- */
/**
* @brief Dumps VM state and terminates execution
* @param vm Pointer to VM instance
* @param reason String describing the failure reason
* @param file Source file where failure occurred
* @param line Line number where failure occurred
*/
static void dump_and_die(VM* vm, const char* reason, const char* file, int line)
{
log_message(LOG_ERROR, "[TEST-RUNNER] abort @ %s:%d (%s)",
file, line, reason ? reason : "no-reason");
vm_debug_dump_state(vm, reason ? reason : "test-failure");
test_fatal_exit();
}
/* Convenience macro so call sites get file:line automatically */
#define DIE(vm, why) dump_and_die((vm), (why), __FILE__, __LINE__)
/* Single gate used by fail-fast paths */
/**
* @brief Handles test failure and exits
* @param vm Pointer to VM instance
* @param module Name of the test module
* @param case_name Name of the test case
* @param input Test input that caused the failure
* @param reason Description of failure reason
*/
static void fail_and_exit(VM* vm,
const char* module,
const char* case_name,
const char* input,
const char* reason)
{
if (module && *module && case_name && *case_name)
{
log_message(LOG_ERROR, "[TEST-RUNNER] FAIL in %s.%s", module, case_name);
}
else if (module && *module)
{
log_message(LOG_ERROR, "[TEST-RUNNER] FAIL in %s", module);
}
if (input && *input)
{
log_message(LOG_ERROR, "[TEST-RUNNER] Input: %s", input);
}
if (reason && *reason)
{
log_message(LOG_ERROR, "[TEST-RUNNER] Reason: %s", reason);
}
/* Dump VM state at point-of-failure and exit */
DIE(vm, reason ? reason : "assertion failed");
}
/* ---------- VM state save/restore ---------- */
/**
* @brief Saves current VM state
* @param vm Pointer to VM instance
* @param dsp Pointer to store data stack pointer
* @param rsp Pointer to store return stack pointer
* @param error Pointer to store error state
* @param mode Pointer to store VM mode
*/
void save_vm_state(VM* vm, int* dsp, int* rsp, int* error, vm_mode_t* mode)
{
*dsp = vm->dsp;
*rsp = vm->rsp;
*error = vm->error;
*mode = vm->mode;
}
/**
* @brief Restores previously saved VM state
* @param vm Pointer to VM instance
* @param dsp Data stack pointer to restore
* @param rsp Return stack pointer to restore
* @param error Error state to restore
* @param mode VM mode to restore
*/
void restore_vm_state(VM* vm, int dsp, int rsp, int error, vm_mode_t mode)
{
/* Restore to the caller-supplied saved state -- every real caller
* (acl_words_test.c, this file's own save/restore pairs) captures
* dsp/rsp/error/mode before running a test specifically so it can be
* put back here. This previously ignored all four parameters and
* hard-reset to a fixed baseline instead, silently not restoring
* anything callers actually saved. */
vm->dsp = dsp;
vm->rsp = rsp;
vm->error = error;
vm->mode = mode;
/* Clear control flow flags to prevent stale state between tests */
vm->exit_colon = 0;
vm->abort_requested = 0;
/* Clear compilation state in case test left system in compile mode */
vm->compiling_word = NULL;
vm->current_executing_entry = NULL;
}
/* ---------- Assertions ---------- */
/**
* @brief Verifies VM stack depth matches expected value
* @param vm Pointer to VM instance
* @param expected_depth Expected stack depth
* @return 1 if assertion passes, 0 if it fails
*/
int assert_stack_depth(VM* vm, int expected_depth)
{
if (vm->dsp == expected_depth)
{
return 1;
}
log_message(LOG_ERROR, "Stack depth mismatch: expected %d, got %d", expected_depth, vm->dsp);
return 0;
}
/**
* @brief Verifies top value on VM stack matches expected value
* @param vm Pointer to VM instance
* @param expected_value Expected value on top of stack
* @return 1 if assertion passes, 0 if it fails
*/
int assert_stack_top(VM* vm, int expected_value)
{
if (vm->dsp < 1)
{
log_message(LOG_ERROR, "Stack underflow when checking top value");
return 0;
}
if (vm->data_stack[vm->dsp - 1] == expected_value)
{
return 1;
}
log_message(LOG_ERROR, "Stack top mismatch: expected %d, got %d",
expected_value, vm->data_stack[vm->dsp - 1]);
return 0;
}
/**
* @brief Verifies VM error state matches expectation
* @param vm Pointer to VM instance
* @param should_have_error Expected error state (1 for error, 0 for no error)
* @return 1 if assertion passes, 0 if it fails
*/
int assert_vm_error(VM* vm, int should_have_error)
{
int has_error = (vm->error != 0);
if (has_error == should_have_error)
{
return 1;
}
if (should_have_error)
{
log_message(LOG_ERROR, "Expected VM error but none occurred");
}
else
{
log_message(LOG_ERROR, "Unexpected VM error: %d", vm->error);
}
return 0;
}
/* ---------- Test execution ---------- */
/* Run a single test case */
/**
* @brief Executes a single test case
* @param vm Pointer to VM instance
* @param word_name Name of the word being tested
* @param test Pointer to test case definition
* @return Test result (PASS, FAIL, SKIP, or ERROR)
*/
TestResult run_single_test(VM* vm, const char* word_name, const TestCase* test)
{
if (!test->implemented)
{
log_test_result(word_name, TEST_SKIP);
return TEST_SKIP;
}
/* Save VM state */
int saved_dsp, saved_rsp, saved_error;
vm_mode_t saved_mode;
save_vm_state(vm, &saved_dsp, &saved_rsp, &saved_error, &saved_mode);
/* Clear error state */
vm->error = 0;
log_message(LOG_TEST, " Running %s.%s: %s", word_name, test->name, test->input);
/* Execute the test */
vm_interpret(vm, test->input);
/* Check results */
TestResult result;
if (test->should_error)
{
if (vm->error != 0)
{
result = TEST_PASS;
log_message(LOG_TEST, " Expected error occurred");
}
else
{
result = TEST_FAIL;
log_message(LOG_ERROR, " Expected error but none occurred");
}
}
else
{
if (vm->error != 0)
{
result = TEST_FAIL;
log_message(LOG_ERROR, " Unexpected VM error: %d", vm->error);
}
else
{
result = TEST_PASS;
log_message(LOG_TEST, " Test passed");
}
}
log_test_result(word_name, result);
if (result == TEST_PASS)
{
log_message(LOG_TEST, " Expected: %s", test->expected);
}
/* Restore VM nightly */
restore_vm_state(vm, saved_dsp, saved_rsp, saved_error, saved_mode);
if (result == TEST_FAIL)
{
log_message(LOG_ERROR, "Test failed: %s.%s", word_name, test->name);
log_message(LOG_ERROR, "Input: %s", test->input);
log_message(LOG_ERROR, "Expected: %s", test->expected);
if (fail_fast != 0)
{
/* Fail fast: dump and exit with real names from this scope */
fail_and_exit(vm, word_name, test->name, test->input, "assertion failed");
}
}
return result;
}
/* Run a complete test suite for one word */
/**
* @brief Executes a complete test suite for a Forth word
* @param vm Pointer to VM instance
* @param suite Pointer to test suite definition
*/
void run_test_suite(VM* vm, const WordTestSuite* suite)
{
log_message(LOG_TEST, "Testing word: %s", suite->word_name);
log_message(LOG_TEST, "------------------------");
/* Save dictionary state before test suite */
DictEntry* saved_latest;
size_t saved_here;
save_dict_state(vm, &saved_latest, &saved_here);
int suite_pass = 0;
int suite_fail = 0;
int suite_skip = 0;
int suite_error = 0;
for (int j = 0; j < suite->test_count && suite->tests[j].name != NULL; j++)
{
const TestCase* test = &suite->tests[j];
if (!test->implemented)
{
suite_skip++;
continue;
}
TestResult result = run_single_test(vm, suite->word_name, test);
switch (result)
{
case TEST_PASS: suite_pass++;
break;
case TEST_FAIL: suite_fail++;
break;
case TEST_SKIP: suite_skip++;
break;
case TEST_ERROR: suite_error++;
break;
}
}
/* Restore dictionary state after test suite to remove test-created words */
restore_dict_state(vm, saved_latest, saved_here);
test_stats_accumulate(suite_pass + suite_fail + suite_skip + suite_error,
suite_pass, suite_fail, suite_skip, suite_error);
log_message(LOG_TEST, " %s: %d passed, %d failed, %d stubs, %d errors",
suite->word_name, suite_pass, suite_fail, suite_skip, suite_error);
}
/* ============================================================================
* run_test_suite_m — contract-aware suite runner
* ============================================================================ */
/**
* @brief Resolves the effective contract for a test case.
*
* First non-NONE source wins: per-test > per-suite > module.
*/
static WordContract resolve_contract(WordContract test_c,
WordContract suite_c,
WordContract module_c)
{
if (test_c.flags) return test_c;
if (suite_c.flags) return suite_c;
return module_c;
}
void run_test_suite_m(VM *vm, const WordTestSuite *suite, WordContract module_contract)
{
log_message(LOG_TEST, "Testing word: %s", suite->word_name);
log_message(LOG_TEST, "------------------------");
DictEntry* saved_latest;
size_t saved_here;
save_dict_state(vm, &saved_latest, &saved_here);
int suite_pass = 0;
int suite_fail = 0;
int suite_skip = 0;
int suite_error = 0;
for (int j = 0; j < suite->test_count && suite->tests[j].name != NULL; j++)
{
const TestCase* test = &suite->tests[j];
if (!test->implemented)
{
suite_skip++;
continue;
}
TestResult result = run_single_test(vm, suite->word_name, test);
switch (result)
{
case TEST_PASS:
suite_pass++;
/* A4' contract check for passing non-error tests */
if (result == TEST_PASS && test->type != TEST_ERROR_CASE)
{
WordContract eff = resolve_contract(test->contract,
suite->suite_contract,
module_contract);
if (eff.flags & CONTRACT_PHYSICS_TRANSPARENT)
{
int ok = check_physics_transparent(vm, suite->word_name,
test->input,
test->should_error,
test->implemented,
eff.fuzz_rounds);
if (!ok)
{
/* Demote to fail: contract violation is a test failure */
suite_pass--;
suite_fail++;
}
}
}
break;
case TEST_FAIL: suite_fail++; break;
case TEST_SKIP: suite_skip++; break;
case TEST_ERROR: suite_error++; break;
}
}
restore_dict_state(vm, saved_latest, saved_here);
test_stats_accumulate(suite_pass + suite_fail + suite_skip + suite_error,
suite_pass, suite_fail, suite_skip, suite_error);
log_message(LOG_TEST, " %s: %d passed, %d failed, %d stubs, %d errors",
suite->word_name, suite_pass, suite_fail, suite_skip, suite_error);
}
/* Print module summary */
/**
* @brief Prints test module execution summary
* @param module_name Name of the test module
* @param pass Number of passed tests
* @param fail Number of failed tests
* @param skip Number of skipped tests
* @param error Number of tests with errors
*/
void print_module_summary(const char* module_name, int pass, int fail, int skip, int error)
{
log_message(LOG_TEST, "=== %s Summary: %d passed, %d failed, %d skipped, %d errors ===",
module_name, pass, fail, skip, error);
}
/* ---------- Dictionary State Management ---------- */
/**
* @brief Saves current dictionary state before test execution
* @param vm Pointer to VM instance
* @param latest Pointer to store latest dictionary entry
* @param here Pointer to store HERE pointer
* @details Captures dictionary state so tests can be cleaned up after execution
*/
void save_dict_state(VM* vm, DictEntry** latest, size_t* here)
{
*latest = vm->latest;
*here = vm->here;
}
/**
* @brief Restores dictionary state after test execution
* @param vm Pointer to VM instance
* @param latest Latest dictionary entry to restore
* @param here HERE pointer to restore
* @details Removes all words added during test by restoring dictionary pointers.
* This prevents test pollution of the dictionary and ensures each test
* starts with a clean slate.
*/
void restore_dict_state(VM* vm, DictEntry* latest, size_t here)
{
/* Restore dictionary pointers to state before test */
vm->latest = latest;
vm->here = here;
/* NOTE: We don't free memory here because the dictionary uses a contiguous
* memory region. The memory is still allocated but unreachable. This is
* safe because tests run in a controlled environment and memory is reclaimed
* when the VM terminates. For long-running test suites, consider periodically
* reinitializing the VM. */
}
+356
View File
@@ -0,0 +1,356 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
Licensed under the Starship License 1.0 (SL-1.0)
*/
/*
* test_contracts.c — Runtime witnesses for Isabelle/HOL axioms A1 and A4'.
*
* All contract checks are compiled away when CONTRACTS_ENABLED != 1 so that
* normal `make test` builds carry zero overhead.
*/
#include "test_contracts.h"
#include "log.h" /* log_message, LOG_ERROR, LOG_TEST */
#include "vm.h" /* VM, vm_interpret, vm_tick, ROLLING_WINDOW_SIZE */
#include <string.h> /* memcpy, memset, memcmp */
/* Global violation counter (defined here; declared extern in test_contracts.h) */
int global_contract_violations = 0;
/*
* Accessor functions — same-TU direct access avoids R_X86_64_REX_GOTPCRELX
* double-dereference in -fPIC PE builds where GOTPCRELX is not relaxed to LEA.
*/
/**
* @brief Return the current count of accumulated contract violations.
*
* Reads @c global_contract_violations directly; same-TU access avoids
* GOT double-dereference in @c -fPIC PE32+ builds where
* @c R_X86_64_REX_GOTPCRELX is not relaxed to a LEA instruction.
*
* @return Number of contract violations recorded since last reset.
*/
int contract_get_violation_count(void) { return global_contract_violations; }
/**
* @brief Reset the contract violation counter to zero.
*
* Call before a fresh round of contract checks so that counts from
* earlier test phases do not accumulate across module boundaries.
*/
void contract_reset_violation_count(void) { global_contract_violations = 0; }
/* ============================================================================
* xorshift64 RNG — deterministic, seeded per-round so each perturbation
* produces a distinct physics state.
* ============================================================================ */
static uint64_t rng_state = 0xDEADBEEFCAFEBABEULL;
/**
* @brief Generate the next pseudo-random 64-bit value via xorshift64.
*
* Uses the global @c rng_state. The xorshift64 parameters (13, 7, 17)
* produce a full-period sequence over all non-zero 64-bit states.
* Caller must seed via @c rng_seed() before starting a new fuzz round
* to ensure different perturbation sequences across rounds.
*
* @return Next pseudo-random 64-bit integer.
*/
static uint64_t contract_rand(void) {
uint64_t x = rng_state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
rng_state = x;
return x;
}
/**
* @brief Seed the RNG for a given fuzz round.
*
* Derives a deterministic seed from the round index mixed with a
* Fibonacci multiplier (@c 0x9E3779B97F4A7C15) so each round produces
* a distinct, reproducible perturbation sequence regardless of how many
* rounds have previously run.
*
* @param round Zero-based fuzz round index.
*/
static void rng_seed(int round) {
rng_state = 0xDEADBEEFCAFEBABEULL ^ ((uint64_t)(unsigned)round * 0x9E3779B97F4A7C15ULL);
}
/* ============================================================================
* Exec-domain capture and comparison
* ============================================================================ */
/**
* @brief Snapshot the execution-domain fields of a VM into an @c ExecResult.
*
* Captures @c dsp, @c rsp, @c error, and the live portion of the data
* stack (indices 0 … dsp−1). The remainder of @c r->stack is zeroed for
* deterministic @c memcmp() comparisons in @c exec_results_equal().
*
* Only execution-domain fields are captured — physics scalars, heartbeat
* state, and rolling-window counters are deliberately excluded.
*
* @param vm VM instance to snapshot.
* @param r Output @c ExecResult struct to populate.
*/
void capture_exec_result(VM *vm, ExecResult *r) {
r->dsp = vm->dsp;
r->rsp = vm->rsp;
r->error = vm->error;
/* copy only the live portion; zero the rest for deterministic memcmp */
int depth = (vm->dsp > 0) ? vm->dsp : 0;
memcpy(r->stack, vm->data_stack, (size_t)depth * sizeof(cell_t));
if (depth < STACK_SIZE)
memset(&r->stack[depth], 0, (size_t)(STACK_SIZE - depth) * sizeof(cell_t));
}
/**
* @brief Compare two @c ExecResult snapshots for execution-domain equality.
*
* Returns 1 only if @c dsp, @c rsp, @c error, and the live stack contents
* are all identical. Stack comparison uses @c memcmp() over the live
* portion (0 … dsp−1 cells); the zeroed tail guarantees no false
* positives from uninitialised padding.
*
* @param a First snapshot.
* @param b Second snapshot.
* @return 1 if execution-domain is identical, 0 if any field differs.
*/
int exec_results_equal(const ExecResult *a, const ExecResult *b) {
if (a->dsp != b->dsp) return 0;
if (a->rsp != b->rsp) return 0;
if (a->error != b->error) return 0;
int depth = (a->dsp > 0) ? a->dsp : 0;
return memcmp(a->stack, b->stack, (size_t)depth * sizeof(cell_t)) == 0;
}
/* ============================================================================
* Physics snapshot / restore / perturb
*
* These cover the key scalars touched by the seven feedback loops. A word
* that reads any of these will produce a different ExecResult after perturb,
* exposing a violation of A4'.
* ============================================================================ */
/**
* @brief Save the key physics scalars touched by the seven feedback loops.
*
* Captures the eight fields that @c perturb_physics() will randomise:
* @c decay_slope_q48, @c effective_window_size, @c tick_target_ns,
* the three heat percentile thresholds, @c prefetch_attempts, and
* @c total_executions. Used as a bracket pair with @c restore_physics()
* around each fuzz round so the VM returns to a known state.
*
* @param vm VM instance to snapshot.
* @param s Output @c PhysicsSnapshot to populate.
*/
void save_physics(VM *vm, PhysicsSnapshot *s) {
s->decay_slope_q48 = vm->decay_slope_q48;
s->eff_window_size = vm->rolling_window.effective_window_size;
s->tick_target_ns = vm->heartbeat.tick_target_ns;
s->heat_25th = vm->heat_threshold_25th;
s->heat_50th = vm->heat_threshold_50th;
s->heat_75th = vm->heat_threshold_75th;
s->prefetch_attempts = vm->pipeline_metrics.prefetch_attempts;
s->total_executions = vm->rolling_window.total_executions;
}
/**
* @brief Restore the physics scalars from a previously saved snapshot.
*
* Writes back the eight fields saved by @c save_physics(), undoing any
* perturbations made by @c perturb_physics(). Called at the end of each
* fuzz round and after the full @c check_physics_transparent() loop so
* the VM state is clean for the next contract check.
*
* @param vm VM instance to restore.
* @param s Source @c PhysicsSnapshot to restore from.
*/
void restore_physics(VM *vm, const PhysicsSnapshot *s) {
vm->decay_slope_q48 = s->decay_slope_q48;
vm->rolling_window.effective_window_size = s->eff_window_size;
vm->heartbeat.tick_target_ns = s->tick_target_ns;
vm->heat_threshold_25th = s->heat_25th;
vm->heat_threshold_50th = s->heat_50th;
vm->heat_threshold_75th = s->heat_75th;
vm->pipeline_metrics.prefetch_attempts = s->prefetch_attempts;
vm->rolling_window.total_executions = s->total_executions;
}
/**
* @brief Randomly perturb key physics scalars to expose A4' violations.
*
* Seeds the RNG via @c rng_seed(@p round) for deterministic but distinct
* perturbations per round, then overwrites the eight tracked physics
* fields with random values that still satisfy their well-formedness
* invariants:
*
* - @c decay_slope_q48: LSB forced to 1 (must be > 0 per @c slope_wf)
* - @c effective_window_size: [1, ROLLING_WINDOW_SIZE] (never 0)
* - @c tick_target_ns: LSB forced to 1 (must be > 0 per @c hb_state_wf)
* - Heat thresholds: arbitrary positive values (no strict invariant)
* - @c prefetch_attempts / @c total_executions: arbitrary counts
*
* After perturbation, running the same FORTH input must produce an
* identical @c ExecResult; divergence is an A4' violation.
*
* @param vm VM instance to perturb.
* @param round Fuzz round index (used to seed the RNG).
*/
void perturb_physics(VM *vm, int round) {
rng_seed(round);
/* decay_slope_q48: must be > 0 per slope_wf */
vm->decay_slope_q48 = contract_rand() | 1ULL;
/* effective_window_size: must be in [1, ROLLING_WINDOW_SIZE] */
uint64_t w = contract_rand() % (uint64_t)ROLLING_WINDOW_SIZE;
vm->rolling_window.effective_window_size = (uint32_t)(w == 0 ? 1 : w);
/* tick_target_ns: must be > 0 per hb_state_wf */
vm->heartbeat.tick_target_ns = contract_rand() | 1ULL;
/* heat thresholds: arbitrary signed values */
vm->heat_threshold_25th = (cell_t)(contract_rand() & 0x7FFF);
vm->heat_threshold_50th = (cell_t)(contract_rand() & 0xFFFF);
vm->heat_threshold_75th = (cell_t)(contract_rand() & 0x1FFFF);
/* pipeline and window counters */
vm->pipeline_metrics.prefetch_attempts = contract_rand();
vm->rolling_window.total_executions = contract_rand();
}
/* ============================================================================
* A4' contract check
* ============================================================================ */
/**
* @brief A4' contract check — verify a word is transparent to physics state.
*
* Axiom A4' (physics transparency): a FORTH word's execution-domain
* result (@c ExecResult) must be identical regardless of the values of
* the seven physics feedback-loop scalars.
*
* Protocol:
* 1. Run @p input with current (unperturbed) physics; capture baseline.
* 2. For each of @p fuzz_rounds rounds: seed RNG, perturb physics,
* re-run @p input, capture result, compare to baseline.
* 3. On divergence, log the violation and increment
* @c global_contract_violations.
* 4. Always restore original physics state before returning.
*
* Error-expected tests (@p should_error) and unimplemented words
* (@p implemented == 0) are skipped: exec-equivalent divergence is
* legitimate for error paths.
*
* @param vm VM instance.
* @param word_name Human-readable word name for diagnostic messages.
* @param input FORTH source string to execute (same each round).
* @param should_error Non-zero if the test is expected to raise an error.
* @param implemented Non-zero if the word is implemented and should be checked.
* @param fuzz_rounds Number of perturbation rounds; 0 → CONTRACT_DEFAULT_FUZZ_ROUNDS.
* @return 1 if all rounds passed (or test skipped), 0 on any violation.
*/
int check_physics_transparent(VM *vm, const char *word_name,
const char *input,
int should_error, int implemented,
int fuzz_rounds) {
/* Skip error-expected tests: exec-equiv may legitimately differ */
if (!implemented || should_error) return 1;
int rounds = (fuzz_rounds > 0) ? fuzz_rounds : CONTRACT_DEFAULT_FUZZ_ROUNDS;
/* --- Baseline run with current (unperturbed) physics --- */
vm->error = 0;
vm->dsp = -1;
vm->rsp = -1;
vm->exit_colon = 0;
vm->abort_requested = 0;
vm_interpret(vm, input);
ExecResult baseline;
capture_exec_result(vm, &baseline);
PhysicsSnapshot saved;
save_physics(vm, &saved);
int ok = 1;
for (int r = 0; r < rounds && ok; r++) {
perturb_physics(vm, r);
vm->error = 0;
vm->dsp = -1;
vm->rsp = -1;
vm->exit_colon = 0;
vm->abort_requested = 0;
vm_interpret(vm, input);
ExecResult fuzzed;
capture_exec_result(vm, &fuzzed);
if (!exec_results_equal(&baseline, &fuzzed)) {
log_message(LOG_ERROR,
"[A4' CONTRACT] VIOLATION: %s '%s' result changed under physics perturbation (round %d)",
word_name, input, r);
log_message(LOG_ERROR,
" baseline dsp=%d fuzzed dsp=%d", baseline.dsp, fuzzed.dsp);
ok = 0;
global_contract_violations++;
}
vm->dsp = -1;
vm->rsp = -1;
vm->error = 0;
}
restore_physics(vm, &saved);
vm->dsp = -1;
vm->rsp = -1;
vm->error = 0;
return ok;
}
/* ============================================================================
* A1 contract check
* ============================================================================ */
/**
* @brief A1 contract check — verify @c vm_tick() does not modify the execution domain.
*
* Axiom A1 (heartbeat isolation): the background heartbeat tick must never
* alter stacks, error state, or any other execution-domain field observable
* by FORTH words. This function captures an @c ExecResult before and after
* one @c vm_tick() call and compares them with @c exec_results_equal().
*
* On violation, logs @c [A1 CONTRACT] VIOLATION with before/after dsp and
* rsp values, and increments @c global_contract_violations.
*
* @param vm VM instance; stacks and error state must already be in a known
* reference state before calling.
* @return 1 if the heartbeat left the execution domain unchanged, 0 on violation.
*/
int assert_heartbeat_safe(VM *vm) {
ExecResult before, after;
capture_exec_result(vm, &before);
vm_tick(vm);
capture_exec_result(vm, &after);
if (!exec_results_equal(&before, &after)) {
log_message(LOG_ERROR,
"[A1 CONTRACT] VIOLATION: vm_tick() modified exec-equiv fields");
log_message(LOG_ERROR,
" dsp before=%d after=%d rsp before=%d after=%d",
before.dsp, after.dsp, before.rsp, after.rsp);
global_contract_violations++;
return 0;
}
return 1;
}
+272
View File
@@ -0,0 +1,272 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "include/test_runner.h"
#include "include/test_contracts.h"
#include "../../include/log.h"
#include <stdint.h>
#ifdef __STARKERNEL__
#include "platform_time.h"
/* Kernel uses sf_monotonic_ns() for timing */
/* strcmp provided by shim.c */
extern int strcmp(const char *a, const char *b);
#else
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#endif
/** @brief Global statistics for test execution */
#ifdef __STARKERNEL__
/* Ensure visibility for GOT-based access with -fPIC -fvisibility=hidden */
__attribute__((visibility("default")))
#endif
TestStats global_test_stats = {0, 0, 0, 0, 0};
/*
* Same-TU accessor — avoids R_X86_64_REX_GOTPCRELX double-dereference in
* -fPIC PE builds where struct access generates GOT-indirect code that the
* PE linker does not relax.
*/
void test_stats_accumulate(int tests, int pass, int fail, int skip, int error)
{
global_test_stats.total_tests += tests;
global_test_stats.total_pass += pass;
global_test_stats.total_fail += fail;
global_test_stats.total_skip += skip;
global_test_stats.total_error += error;
}
/** @brief Flag indicating if benchmark mode is enabled */
static int benchmark_mode = 0;
/** @brief Number of iterations for benchmark tests */
static int benchmark_iterations = 1000;
/**
* @brief Get current time in nanoseconds
*
* Uses platform-specific timing: sf_monotonic_ns() on kernel, clock() on hosted.
*
* @return Current time in nanoseconds, or 0 on error
*/
static uint64_t get_time_ns(void) {
#ifdef __STARKERNEL__
return sf_monotonic_ns();
#else
clock_t t = clock();
if (t == (clock_t)(-1)) {
return 0; /* Error case */
}
/* Convert to nanoseconds: (clock_ticks / CLOCKS_PER_SEC) * 1,000,000,000 */
return (uint64_t)((double) t / CLOCKS_PER_SEC * 1000000000.0);
#endif
}
/* Test modules in POST (Power-On Self Test) order:
* 1. Unit tests (foundational math/algorithms) run FIRST
* 2. Dictionary tests (FORTH word validation) run after POST passes
*/
static TestModule test_modules[] = {
/* === Dictionary Tests: FORTH-79 Word Validation === */
{"Stack Words", NULL, 0, run_stack_words_tests}, /* Module 5 */
{"Return Stack Words", NULL, 0, run_return_stack_words_tests}, /* Module 6 */
{"Memory Words", NULL, 0, run_memory_words_tests}, /* Module 7 */
{"Arithmetic Words", NULL, 0, run_arithmetic_words_tests}, /* Module 8 */
{"Logical Words", NULL, 0, run_logical_words_tests}, /* Module 9 */
{"Mixed Arithmetic Words", NULL, 0, run_mixed_arithmetic_words_tests}, /* Module 10 */
{"Double Words", NULL, 0, run_double_words_tests}, /* Module 11 */
{"Format Words", NULL, 0, run_format_words_tests}, /* Module 12 */
{"String Words", NULL, 0, run_string_words_tests}, /* Module 13 */
{"I/O Words", NULL, 0, run_io_words_tests}, /* Module 14 */
{"Block Words", NULL, 0, run_block_words_tests}, /* Module 15 */
{"Dictionary Words", NULL, 0, run_dictionary_words_tests}, /* Module 16 */
{"Dictionary Manipulation Words", NULL, 0, run_dictionary_manipulation_words_tests}, /* Module 17 */
{"Vocabulary Words", NULL, 0, run_vocabulary_words_tests}, /* Module 18 */
{"System Words", NULL, 0, run_system_words_tests}, /* Module 19 */
{"Defining Words", NULL, 0, run_defining_words_tests}, /* Module 20 */
{"Control Words", NULL, 0, run_control_words_tests}, /* Module 21 */
{"StarForth Words", NULL, 0, run_starforth_words_tests}, /* Module 22 */
{"ACL Words", NULL, 0, run_acl_words_tests}, /* Module 23: Word-Level ACL System */
{"Mama FORTH Words", NULL, 0, run_mama_forth_words_tests}, /* Module 24: Capsule System M7.1 */
{"Q48.16 Words", NULL, 0, run_q48_words_tests}, /* Module 25: Q48.16 Fixed-Point Arithmetic */
{"Inference Engine Words", NULL, 0, run_inference_words_tests}, /* Module 26: SSM Inference (proof-covered subset) */
{"Physics Freeze Words", NULL, 0, run_physics_freeze_words_tests}, /* Module 27: Physics Freeze (proof-covered subset) */
{NULL, NULL, 0, NULL} /* End marker */
};
/**
* @brief Enable benchmark mode for test execution
*
* @param iterations Number of times each test should be repeated
*/
void enable_benchmark_mode(int iterations) {
benchmark_mode = 1;
benchmark_iterations = iterations;
log_message(LOG_INFO, "Benchmark mode enabled: %d iterations per module", iterations);
}
/**
* @brief Run all test modules with optional benchmarking
*
* Executes all registered test modules sequentially, collecting statistics
* and/or benchmark data depending on the current mode.
*
* @param vm Pointer to the Forth virtual machine instance
*/
void run_all_tests(VM *vm) {
log_message(LOG_INFO, "Starting comprehensive FORTH-79 modular test suite...");
log_message(LOG_INFO, "==============================================");
/* Reset global statistics */
global_test_stats.total_tests = 0;
global_test_stats.total_pass = 0;
global_test_stats.total_fail = 0;
global_test_stats.total_skip = 0;
global_test_stats.total_error = 0;
contract_reset_violation_count();
/* Run each test module */
for (int i = 0; test_modules[i].module_name != NULL; i++) {
TestModule const*module = &test_modules[i];
log_message(LOG_INFO, "\n=== Testing Module: %s ===", module->module_name);
if (module->run_module_tests != NULL) {
module->run_module_tests(vm);
} else {
log_message(LOG_WARN, "Module %s: No tests implemented yet", module->module_name);
}
}
/* Print test summary */
log_message(LOG_INFO, "FINAL TEST SUMMARY:");
log_message(LOG_INFO, " Total tests run: %d", global_test_stats.total_tests);
log_message(LOG_INFO, " Passed: %d", global_test_stats.total_pass);
log_message(LOG_INFO, " Failed: %d", global_test_stats.total_fail);
log_message(LOG_INFO, " Stubs (not yet implemented): %d", global_test_stats.total_skip);
log_message(LOG_INFO, " Errors: %d", global_test_stats.total_error);
if (global_test_stats.total_fail == 0 && global_test_stats.total_error == 0) {
log_message(LOG_INFO, "ALL IMPLEMENTED TESTS PASSED!");
} else {
log_message(LOG_ERROR, "%d tests FAILED or had ERRORS!",
global_test_stats.total_fail + global_test_stats.total_error);
}
if (contract_get_violation_count() > 0) {
log_message(LOG_ERROR, " Contract violations (A4'/A1): %d *** AXIOM WITNESSES FAILED ***",
contract_get_violation_count());
} else {
log_message(LOG_INFO, " Contract checks (A4'/A1): all passed");
}
log_message(LOG_INFO, "==============================================");
}
/**
* @brief Run tests for a specific module
*
* @param vm Pointer to the Forth virtual machine instance
* @param module_name Name of the module to test
*/
void run_module_tests(VM *vm, const char *module_name) {
for (int i = 0; test_modules[i].module_name != NULL; i++) {
if (strcmp(test_modules[i].module_name, module_name) == 0) {
if (benchmark_mode) {
log_message(LOG_INFO, "Benchmarking module: %s", module_name);
if (test_modules[i].run_module_tests != NULL) {
/* Warmup */
test_modules[i].run_module_tests(vm);
/* Benchmark */
uint64_t start_time = get_time_ns();
for (int iter = 0; iter < benchmark_iterations; iter++) {
test_modules[i].run_module_tests(vm);
}
uint64_t end_time = get_time_ns();
uint64_t total_time = end_time - start_time;
double runs_per_second = (double) benchmark_iterations * 1000000000.0 / total_time;
log_message(LOG_INFO, " %.0f runs/sec | %.1f μs/run",
runs_per_second,
(double) total_time / benchmark_iterations / 1000.0);
} else {
log_message(LOG_WARN, "No tests implemented for module: %s", module_name);
}
} else {
log_message(LOG_INFO, "Running tests for module: %s", module_name);
if (test_modules[i].run_module_tests != NULL) {
test_modules[i].run_module_tests(vm);
} else {
log_message(LOG_WARN, "No tests implemented for module: %s", module_name);
}
}
return;
}
}
log_message(LOG_ERROR, "Unknown test module: %s", module_name);
}
/**
* @brief Run tests for a specific Forth word
*
* Searches all modules for tests related to the specified word and executes them.
*
* @param vm Pointer to the Forth virtual machine instance
* @param word_name Name of the Forth word to test
*/
void run_word_tests(VM *vm, const char *word_name) {
(void)vm;
log_message(LOG_INFO, "Searching for tests for word: %s", word_name);
int found = 0;
for (int i = 0; test_modules[i].module_name != NULL; i++) {
/* This would need to be implemented in each module */
/* For now, just run all tests - individual word testing can be added later */
}
if (!found) {
log_message(LOG_WARN, "No tests found for word: %s", word_name);
}
}
+981
View File
@@ -0,0 +1,981 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* vm.c (vm_core) - The VM is: state, invariants, execution spine
*
* Contains:
* - instruction dispatch loop
* - stack ops
* - dictionary lookup
* - word execution
* - VM flags / modes
* - anything that mutates VM state directly
*
* Rule: If it touches the stacks, it lives here.
*/
#include "vm_internal.h"
#include "../include/log.h"
#include "../include/profiler.h"
#include "../include/physics_metadata.h"
#include "../include/physics_execution_hooks.h"
#include "../include/physics_hotwords_cache.h"
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* ====================== Base helpers ======================= */
/**
* @brief Read the current number base from the VM.
*
* Prefers the cell stored at @c vm->base_addr (the canonical FORTH
* @c BASE variable) if it is aligned and in the valid range [2, 36].
* Falls back to the host-mirror @c vm->base if the cell is out of
* range, and finally returns 10 if both are invalid or @p vm is NULL.
*
* The double-read (cell + mirror) keeps the VM consistent when C code
* modifies @c vm->base directly without going through @c vm_set_base().
*
* @param vm VM instance to query (may be NULL; returns 10 in that case).
* @return Current number base in range [2, 36].
*/
unsigned vm_get_base(const VM* vm)
{
if (!vm) return 10u;
/* Prefer VM cell if valid */
vaddr_t a = vm->base_addr;
if ((a % sizeof(cell_t)) == 0 && (size_t)a + sizeof(cell_t) <= VM_MEMORY_SIZE)
{
cell_t v = vm_load_cell((VM*)vm, a); /* cast-away const for accessor */
if (v >= 2 && v <= 36) return (unsigned)v;
}
/* Fallback to host mirror */
if (vm->base >= 2 && vm->base <= 36) return (unsigned)vm->base;
return 10u;
}
/**
* @brief Set the VM number base and update both the cell and the host mirror.
*
* Clamps @p b to [2, 36] (replacing out-of-range values with 10), then
* writes to the FORTH @c BASE variable cell at @c vm->base_addr and to
* the host-mirror field @c vm->base so both sources agree.
*
* @param vm VM instance to update.
* @param b Desired number base; clamped to [2, 36].
*/
void vm_set_base(VM* vm, unsigned b)
{
if (!vm) return;
if (b < 2 || b > 36) b = 10;
vm_store_cell(vm, vm->base_addr, (cell_t)b);
vm->base = (cell_t)b; /* host mirror */
}
/* vm_init and vm_cleanup moved to vm_bootstrap.c */
/* vm_tick* and heartbeat functions moved to vm_time.c */
/**
* @brief One-time write of the Zuse cert (seed + derived pubkey) -- blows
* the fuse.
*
* Deliberately not backed by a dictionary CONSTANT: ACL-PIN only blocks
* redefinition (vm_create_word shadowing), not a >BODY-then-store on the
* word's data field, so a pinned CONSTANT is not actually immutable against
* FORTH-level tampering. Keeping the value in a VM struct field with no
* corresponding FORTH store word closes that path entirely -- see
* FABRIC-2.md's Milestone 4 mint-then-pin writeup for the finding.
*
* @param vm VM instance.
* @param seed Ed25519 seed, 32 bytes (the private identity).
* @param pubkey Ed25519 public key derived from seed, 32 bytes.
* @return 0 on success; -1 if already installed (fuse already blown).
*/
int vm_zuse_cert_install(VM* vm, const uint8_t seed[32], const uint8_t pubkey[32])
{
if (!vm) return -1;
if (vm->zuse_cert_installed) return -1;
memcpy(vm->zuse_cert_seed, seed, 32);
memcpy(vm->zuse_cert_pubkey, pubkey, 32);
vm->zuse_cert_installed = 1;
return 0;
}
/* ====================== Parser / number ======================= */
/**
* @brief Parse the next whitespace-delimited token from the input buffer.
*
* Skips leading whitespace (space, tab, CR, LF), then extracts characters
* until the next whitespace or end of buffer, storing up to @p max_len − 1
* characters in @p word followed by a NUL terminator. Advances
* @c vm->input_pos past the consumed token.
*
* @param vm VM instance; @c vm->input_buffer / @c input_pos / @c input_length
* must be populated (typically by @c vm_interpret()).
* @param word Output buffer for the parsed token.
* @param max_len Size of @p word in bytes (including NUL terminator).
* @return Number of characters written (excluding NUL), or 0 if
* the buffer is exhausted or @p vm / @p word is NULL.
*/
int vm_parse_word(VM* vm, char* word, size_t max_len)
{
if (!vm || !word || max_len == 0) return 0;
/* Skip whitespace */
while (vm->input_pos < vm->input_length)
{
char c = vm->input_buffer[vm->input_pos];
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') break;
vm->input_pos++;
}
if (vm->input_pos >= vm->input_length) return 0;
size_t len = 0;
while (vm->input_pos < vm->input_length && len < max_len - 1)
{
char c = vm->input_buffer[vm->input_pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') break;
word[len++] = c;
vm->input_pos++;
}
word[len] = '\0';
return (int)len;
}
/**
* @brief Attempt to parse a string as an integer in the current number base.
*
* Reads the current base from @c vm_get_base(), then converts each
* character of @p s (case-insensitive, digits 0–9 and letters A–Z/a–z)
* to a digit value. Returns 0 immediately on any character that is not
* a valid digit in the current base, or on an empty string after the
* optional sign.
*
* Handles an optional leading @c '+' or @c '-' sign. The result is a
* full-width @c cell_t (sign extended for negative values).
*
* @param vm VM instance (used for base only; may be NULL, in which case
* base defaults to 10).
* @param s NUL-terminated string to parse.
* @param out Pointer to receive the parsed value on success.
* @return 1 on successful parse, 0 on any parse failure.
*/
int vm_parse_number(VM* vm, const char* s, cell_t* out)
{
if (!s || !*s || !out) return 0;
unsigned base = vm_get_base(vm);
int neg = 0;
if (*s == '+' || *s == '-')
{
neg = (*s == '-');
s++;
if (!*s) return 0;
}
unsigned long long acc = 0;
int any = 0;
for (const char* p = s; *p; ++p)
{
unsigned d;
unsigned char c = (unsigned char)*p;
if (c >= '0' && c <= '9') d = (unsigned)(c - '0');
else if (c >= 'A' && c <= 'Z') d = 10u + (unsigned)(c - 'A');
else if (c >= 'a' && c <= 'z') d = 10u + (unsigned)(c - 'a');
else return 0;
if (d >= base) return 0;
acc = acc * base + d;
any = 1;
}
if (!any) return 0;
cell_t v = (cell_t)acc;
if (neg) v = (cell_t)(-v);
*out = v;
return 1;
}
/* ====================== Compile state ======================= */
/**
* @brief Begin a colon definition and switch the VM to compile mode.
*
* Creates a new dictionary entry for @p name via @c vm_create_word(),
* marks it @c WORD_SMUDGED so it is invisible to lookups during
* compilation, sets the VM mode to @c MODE_COMPILE, and writes the
* current @c HERE address into the entry's data field as the start of
* the threaded body.
*
* The SMUDGE flag is cleared by @c vm_exit_compile_mode() when the
* semicolon is encountered.
*
* @param vm VM instance.
* @param name Word name string (does not need to be NUL-terminated).
* @param len Length of @p name; clamped to @c WORD_NAME_MAX.
*/
void vm_enter_compile_mode(VM* vm, const char* name, size_t len)
{
if (!vm) return;
vm->mode = MODE_COMPILE;
vm->state_var = -1;
vm_store_cell(vm, vm->state_addr, vm->state_var);
if (len > WORD_NAME_MAX) len = WORD_NAME_MAX;
memcpy(vm->current_word_name, name, len);
vm->current_word_name[len] = '\0';
/* Create colon word header with code pointer = execute_colon_word */
DictEntry* de = vm_create_word(vm, name, len, execute_colon_word);
vm->compiling_word = de;
if (!de)
{
vm->error = 1;
return;
}
de->flags |= WORD_SMUDGED;
/* DF (first data cell) will hold the VM-relative address of threaded body */
vm_align(vm);
cell_t* df = vm_dictionary_get_data_field(de);
if (!df)
{
vm->error = 1;
return;
}
*df = (cell_t)(int64_t)((vaddr_t)vm->here);
log_message(LOG_DEBUG, ": started '%s' at HERE=%zu", vm->current_word_name, vm->here);
}
/**
* @brief Append a word reference (DictEntry*) to the current threaded body.
*
* Aligns @c HERE, then allots one @c cell_t slot and stores the
* @c DictEntry* as a cell. This is the fundamental compilation step used
* by the inner interpreter: each slot in the threaded body is a pointer
* to the @c DictEntry of the word to call.
*
* Sets @c vm->error if the VM is not in compile mode, @p entry is NULL,
* or allotment fails.
*
* @param vm VM instance in compile mode.
* @param entry Dictionary entry of the word to compile; must not be NULL.
*/
void vm_compile_word(VM* vm, DictEntry* entry)
{
if (!vm || vm->mode != MODE_COMPILE) return;
if (!entry)
{
vm->error = 1;
return;
}
vm_align(vm);
cell_t* slot = (cell_t*)vm_allot(vm, sizeof(cell_t));
if (!slot)
{
vm->error = 1;
return;
}
*slot = (cell_t)(uintptr_t)
entry; /* threaded code stores DictEntry* as cell */
}
/**
* @brief Compile a literal value into the current threaded body (or push it).
*
* In compile mode, appends the @c LIT word reference followed by the
* literal value so the inner interpreter will push @p value at runtime.
* In interpret mode, pushes @p value directly onto the data stack.
*
* Sets @c vm->error if @c LIT cannot be found in the dictionary.
*
* @param vm VM instance.
* @param value Cell value to compile or push.
*/
void vm_compile_literal(VM* vm, cell_t value)
{
if (!vm) return;
if (vm->mode != MODE_COMPILE)
{
vm_push(vm, value);
return;
}
DictEntry* LIT = vm_find_word(vm, "LIT", 3);
if (!LIT)
{
vm->error = 1;
log_message(LOG_ERROR, "LIT not found");
return;
}
vm_compile_word(vm, LIT);
vm_align(vm);
cell_t* val = (cell_t*)vm_allot(vm, sizeof(cell_t));
if (!val)
{
vm->error = 1;
return;
}
*val = value;
}
/**
* @brief Compile a call to a word identified by its C function pointer.
*
* Looks up the @c DictEntry whose @c func field matches @p func via
* @c vm_dictionary_find_by_func(), then delegates to @c vm_compile_word().
* Used by generating words (e.g., @c DOES>) that need to embed a specific
* runtime routine into a compiled definition without knowing its name.
*
* Sets @c vm->error if not in compile mode or if no matching entry is found.
*
* @param vm VM instance in compile mode.
* @param func C function pointer identifying the word to compile.
*/
void vm_compile_call(VM* vm, word_func_t func)
{
if (!vm || vm->mode != MODE_COMPILE)
{
vm->error = 1;
return;
}
DictEntry* entry = vm_dictionary_find_by_func(vm, func);
if (!entry)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_compile_call: entry not found");
return;
}
vm_compile_word(vm, entry);
}
/**
* @brief Compile an @c EXIT reference into the current threaded body.
*
* Appends the @c EXIT word entry so that when the threaded body is
* executed, @c execute_colon_word() sets @c vm->exit_colon and unwinds
* the inner loop. Used by control-flow generating words (e.g., @c IF
* optimisation paths) that need an explicit early-return.
*
* Sets @c vm->error if not in compile mode or if @c EXIT is not found.
*
* @param vm VM instance in compile mode.
*/
void vm_compile_exit(VM* vm)
{
if (!vm || vm->mode != MODE_COMPILE) return;
DictEntry* EXIT = vm_find_word(vm, "EXIT", 4);
if (!EXIT)
{
vm->error = 1;
log_message(LOG_ERROR, "EXIT not found");
return;
}
vm_compile_word(vm, EXIT);
}
/**
* @brief Finalise a colon definition and return to interpret mode.
*
* Appends a final @c EXIT reference, clears the @c WORD_SMUDGED flag so
* the new word becomes visible in the dictionary, and sets @c WORD_COMPILED.
* Also computes the word's mass (header + body bytes) for the physics
* engine and calls @c physics_metadata_refresh_state().
*
* Resets @c vm->mode to @c MODE_INTERPRET, clears @c vm->state_var,
* and NULLs @c vm->compiling_word.
*
* Sets @c vm->error if @p vm has no active @c compiling_word or if
* @c EXIT cannot be found.
*
* @param vm VM instance currently in compile mode.
*/
void vm_exit_compile_mode(VM* vm)
{
if (!vm || !vm->compiling_word)
{
vm->error = 1;
return;
}
DictEntry* EXIT = vm_find_word(vm, "EXIT", 4);
if (!EXIT)
{
vm->error = 1;
log_message(LOG_ERROR, "EXIT not found");
return;
}
vm_compile_word(vm, EXIT);
vm->compiling_word->flags &= ~WORD_SMUDGED;
vm->compiling_word->flags |= WORD_COMPILED;
/* Cache coherence: the definition just became visible and now shadows
* any older word of the same name the hot-words cache may still hold. */
hotwords_cache_evict_name(vm->hotwords_cache,
vm->compiling_word->name,
vm->compiling_word->name_len);
/* Inherit creator's ACL from session context:
* zuse-compiled words bypass ACL-RECHECK on first execution (ACL_TTL_OPEN).
* Non-zuse words keep ttl=0, triggering ACL-RECHECK on first execution. */
if (vm->zuse_session) {
vm->compiling_word->acl_ttl = ACL_TTL_OPEN;
vm->compiling_word->acl_allow = 1;
}
cell_t* df = vm_dictionary_get_data_field(vm->compiling_word);
if (df)
{
uint64_t header_bytes = (uint64_t)(((uint8_t*)df + sizeof(cell_t)) - (uint8_t*)vm->compiling_word);
uint64_t body_start = (uint64_t)(vaddr_t)(uint64_t)(*df);
uint64_t here_bytes = (uint64_t)vm->here;
uint64_t body_bytes = (here_bytes >= body_start) ? (here_bytes - body_start) : 0;
uint64_t total = header_bytes + body_bytes;
uint32_t mass = (total > UINT32_MAX) ? UINT32_MAX : (uint32_t)total;
physics_metadata_set_mass(vm->compiling_word, mass);
}
physics_metadata_refresh_state(vm->compiling_word);
vm->mode = MODE_INTERPRET;
vm->state_var = 0;
vm_store_cell(vm, vm->state_addr, vm->state_var);
vm->compiling_word = NULL;
log_message(LOG_DEBUG, "; end definition");
}
/* ====================== Inner interpreter ======================= */
/*
Threaded code layout (compiled by vm_compile_word / vm_compile_literal):
DF cell (in DictEntry) holds a VM address (vaddr_t) of the first code cell.
Each code cell is a cell_t that encodes a DictEntry* (for a word to call),
or is a literal payload following a compiled LIT word.
Control-flow runtime words (e.g., (BRANCH), (0BRANCH), (DO), loops) are
responsible for *modifying the IP stored at the top of the return stack*.
The inner interpreter saves the "next ip" on the return stack before
calling the word; after the word returns, we pop the possibly-modified IP
and continue. This matches your runtime branch helpers' contract.
IMPORTANT: EXIT behavior —
Words implement EXIT by setting vm->exit_colon = 1 (one-shot).
We honor that flag here to unwind the *current* colon only,
without disturbing the caller’s R-stack frame.
*/
/**
* @brief Cold-path ACL enforcement called when a word's TTL counter hits zero.
*
* Looks up the FORTH word @c ACL-RECHECK in the dictionary. If found,
* calls it with @c emergency_console temporarily set to 1 so the recheck
* itself cannot be denied by the ACL system. The FORTH word is expected
* to update @c entry->acl_allow and @c entry->acl_ttl before returning.
*
* If @c ACL-RECHECK is not yet loaded (early boot before @c ACL.4th runs),
* the function **fails open**: sets @c acl_allow=1 and @c acl_ttl=ACL_TTL_OPEN
* so execution continues unimpeded. The same fail-open behaviour applies
* if @c ACL-RECHECK itself raises an error (error flag is cleared).
*
* This function is the cold path; the hot path in @c execute_colon_word()
* and @c vm_interpret_word() simply decrements @c acl_ttl.
*
* @param vm VM instance.
* @param entry Dictionary entry whose TTL has expired and needs rechecking.
*/
static void acl_recheck(VM *vm, DictEntry *entry)
{
DictEntry *recheck = vm_find_word(vm, "ACL-RECHECK", 11);
if (!recheck || !recheck->func) {
/* ACL.4th not yet loaded — fail open */
entry->acl_allow = 1;
entry->acl_ttl = ACL_TTL_OPEN;
return;
}
/* Push XT, call ACL-RECHECK with emergency bypass so it cannot deny itself */
uint8_t saved_ec = vm->emergency_console;
vm->emergency_console = 1;
vm_push(vm, (cell_t)(uintptr_t)entry);
DictEntry *saved_ce = vm->current_executing_entry;
vm->current_executing_entry = recheck;
recheck->func(vm);
vm->current_executing_entry = saved_ce;
vm->emergency_console = saved_ec;
/* If ACL-RECHECK itself errored, fail open and clear the error */
if (vm->error) {
vm->error = 0;
entry->acl_allow = 1;
entry->acl_ttl = ACL_TTL_OPEN;
log_message(LOG_WARN, "acl_recheck: ACL-RECHECK errored for '%.*s'; failing open",
(int)entry->name_len, entry->name);
}
}
/**
* @brief Inner interpreter for threaded colon definitions.
*
* Runs the body of a colon-defined word whose @c DictEntry points to a
* sequence of @c DictEntry* cells in VM memory (indirect threaded code).
* The interpreter pointer (IP) is maintained on the return stack so that
* runtime control-flow words (@c (BRANCH), @c (0BRANCH), @c (DO), etc.)
* can redirect execution by modifying the top-of-return-stack IP.
*
* **Execution loop per iteration:**
* 1. Read @c DictEntry* @c w from @c *ip.
* 2. Run physics pre-execute hooks (heat, decay, pipelining).
* 3. ACL hot-path check (decrement TTL; call @c acl_recheck() on zero).
* 4. Push @c ip+1 onto the return stack as the resume point.
* 5. Call @c w->func(vm).
* 6. Run physics post-execute hooks.
* 7. Check @c vm->exit_colon (set by @c EXIT): pop and discard the saved
* IP, then return to caller.
* 8. Check @c vm->abort_requested: unwind and return immediately.
* 9. Pop the (possibly modified) IP from the return stack and repeat.
*
* @c vm->ecw_nesting is incremented on entry and decremented on every
* return path to enable recursive colon calls.
*
* @param vm VM instance; @c vm->current_executing_entry must point to
* the @c DictEntry being executed.
*/
void execute_colon_word(VM* vm)
{
if (!vm || !vm->current_executing_entry) return;
DictEntry* entry = vm->current_executing_entry;
cell_t* df = vm_dictionary_get_data_field(entry);
if (!df) { vm->error = 1; return; }
vaddr_t body_addr = (vaddr_t)(uint64_t)(*df);
cell_t* ip = (cell_t*)vm_ptr(vm, body_addr);
if (!ip) { vm->error = 1; return; }
DictEntry* prev = NULL;
vm->ecw_nesting++;
for (;;)
{
DictEntry* w = (DictEntry*)(uintptr_t)(*ip);
/* Physics: pre-execution hooks (decay, heat, pipelining) */
physics_pre_execute(vm, w, prev);
profiler_word_count(w);
/* ACL two-level check (hot path: decrement TTL; cold path: recheck) */
if (w && !vm->emergency_console) {
if (w->acl_ttl == 0)
acl_recheck(vm, w);
else
w->acl_ttl--;
if (!w->acl_allow) {
log_message(LOG_WARN, "ACL: denied '%.*s'", (int)w->name_len, w->name);
vm->dsp = -1;
vm->rsp = -1;
vm->error = 1;
return;
}
}
/* Advance IP and save resume point */
ip++;
vm_rpush(vm, (cell_t)(uintptr_t)ip);
if (vm->error) { vm->ecw_nesting--; return; }
/* Execute the word */
vm->current_executing_entry = w;
if (w && w->func)
{
profiler_word_enter(w);
w->func(vm);
physics_post_execute(vm, w);
}
else
{
const char *pname = (entry && entry->name_len > 0) ? entry->name : "?";
if (!w)
log_message(LOG_ERROR, "execute_colon_word: NULL DictEntry* in '%s' (ip-1=%p)",
pname, (void *)(ip - 1));
else
log_message(LOG_ERROR, "execute_colon_word: null func for '%s' in '%s' (ip-1=%p)",
w->name, pname, (void *)(ip - 1));
vm->error = 1;
}
vm->current_executing_entry = entry;
prev = w;
if (vm->error) { vm->ecw_nesting--; return; }
/* ABORT clears stacks and unwinds to QUIT: leave the flag set so
* every enclosing colon-word frame (this function is recursive --
* every colon word's func is execute_colon_word) also sees it and
* also unwinds, instead of consuming it at the first frame that
* notices. The outermost caller (vm_interpret et al.) is
* responsible for finally clearing it. */
if (vm->abort_requested) { vm->ecw_nesting--; return; }
/* EXIT discards saved IP and returns */
if (vm->exit_colon)
{
vm->exit_colon = 0;
(void)vm_rpop(vm);
vm->ecw_nesting--;
return;
}
/* Resume at (possibly modified) IP from return stack */
ip = (cell_t*)(uintptr_t)vm_rpop(vm);
if (vm->error) { vm->ecw_nesting--; return; }
}
}
/* ====================== Outer interpreter ======================= */
/**
* @brief Interpret or compile a single word token.
*
* This is the outer interpreter's per-token dispatch:
*
* 1. **Dictionary lookup**: vocabulary-aware search, falling back to the
* canonical flat dictionary. Physics heat is recorded via
* @c physics_on_lookup().
* 2. **Compile mode, non-immediate**: append the entry to the current
* threaded body via @c vm_compile_word().
* 3. **Interpret mode, or immediate in compile mode**: ACL check (hot-path
* TTL decrement / cold-path @c acl_recheck()), then call @c entry->func().
* 4. **Number literal**: if no dictionary match, attempt @c vm_parse_number().
* In compile mode, compile as a literal; in interpret mode, push directly.
* 5. **Error**: unknown token — sets @c vm->error.
*
* @param vm VM instance.
* @param word_str Token string (need not be NUL-terminated).
* @param len Length of @p word_str in bytes.
*/
void vm_interpret_word(VM* vm, const char* word_str, size_t len)
{
if (!vm || !word_str) return;
log_message(LOG_DEBUG, "INTERPRET: '%.*s' (mode=%s)",
(int)len, word_str,
vm->mode == MODE_COMPILE ? "COMPILE" : "INTERPRET");
/* Vocabulary-aware lookup with fallback to canonical dictionary */
extern DictEntry* vm_vocabulary_find_word(VM* vm, const char* name, size_t nlen);
DictEntry* entry = vm_vocabulary_find_word(vm, word_str, len);
DictEntry* canon = vm_find_word(vm, word_str, len);
if (!entry) entry = canon;
if (entry)
{
/* Physics: track lookup (heat, decay) */
physics_on_lookup(vm, entry, canon);
int is_immediate = ((entry->flags & WORD_IMMEDIATE) ||
(canon && (canon->flags & WORD_IMMEDIATE)));
if (vm->mode == MODE_COMPILE && !is_immediate)
{
log_message(LOG_DEBUG, "COMPILE: '%.*s'", (int)len, word_str);
vm_compile_word(vm, entry);
return;
}
log_message(LOG_DEBUG, "EXECUTE: '%.*s'", (int)len, word_str);
vm->current_executing_entry = entry;
profiler_word_count(entry);
/* ACL check on outer-interpreter path */
if (!vm->emergency_console) {
if (entry->acl_ttl == 0)
acl_recheck(vm, entry);
else
entry->acl_ttl--;
if (!entry->acl_allow) {
log_message(LOG_WARN, "ACL: denied '%.*s'", (int)len, word_str);
vm->dsp = -1;
vm->rsp = -1;
vm->error = 1;
vm->current_executing_entry = NULL;
return;
}
}
if (entry->func)
{
profiler_word_enter(entry);
entry->func(vm);
physics_post_execute(vm, entry);
}
else
{
log_message(LOG_ERROR, "NULL func for '%.*s'", (int)len, word_str);
vm->error = 1;
}
vm->current_executing_entry = NULL;
return;
}
/* Try parsing as number */
cell_t value;
if (vm_parse_number(vm, word_str, &value))
{
log_message(LOG_DEBUG, "NUMBER: '%.*s' = %ld", (int)len, word_str, (long)value);
if (vm->mode == MODE_COMPILE)
vm_compile_literal(vm, value);
else
vm_push(vm, value);
return;
}
log_message(LOG_ERROR, "UNKNOWN WORD: '%.*s'", (int)len, word_str);
vm->error = 1;
}
/**
* @brief Interpret a NUL-terminated string of FORTH source code.
*
* Copies @p input into @c vm->input_buffer (capped at
* @c INPUT_BUFFER_SIZE − 1 bytes, NUL-terminated), then drives the
* word-parse / token-dispatch loop until the buffer is exhausted or
* @c vm->error is set. Each token is dispatched to @c vm_interpret_word().
*
* This is the top-level entry point used by the REPL, @c INCLUDE, and
* inline @c -c execution. It does not reset @c vm->error before running —
* the caller is responsible for clearing error state if desired.
*
* @param vm VM instance.
* @param input NUL-terminated FORTH source string to interpret.
*/
void vm_interpret(VM* vm, const char* input)
{
if (!vm || !input) return;
/* Load into input buffer (cap + NUL) */
size_t n = 0, cap = INPUT_BUFFER_SIZE ? INPUT_BUFFER_SIZE - 1 : 0;
while (n < cap)
{
char c = input[n];
vm->input_buffer[n] = c;
if (c == '\0') break;
++n;
}
if (n == cap) vm->input_buffer[n] = '\0';
vm->input_length = n;
vm->input_pos = 0;
char word[64];
size_t wlen;
/* !abort_requested: ABORT unwinds all the way to QUIT -- stop parsing
* further words in this input the moment it's seen. Flag is left set
* on return; caller (REPL, exec_block_with_retry, etc.) consumes it. */
while (!vm->error && !vm->abort_requested &&
(wlen = (size_t)vm_parse_word(vm, word, sizeof(word))) > 0)
{
vm_interpret_word(vm, word, wlen);
}
}
/* ====================== VM memory helpers ======================= */
/**
* @brief Validate that a VM address range lies within @c vm->memory.
*
* Checks that [addr, addr+len) is fully contained within the
* @c VM_MEMORY_SIZE-byte flat address space. The check is done with
* unsigned arithmetic to avoid UB on overflow: if @p len alone exceeds
* @c VM_MEMORY_SIZE the range is immediately rejected.
*
* @param vm VM instance; returns 0 if NULL or memory not allocated.
* @param addr Start of the range (inclusive).
* @param len Number of bytes in the range.
* @return 1 if the full range [addr, addr+len) is in bounds; 0 otherwise.
*/
int vm_addr_ok(struct VM* vm, vaddr_t addr, size_t len)
{
if (!vm || !vm->memory) return 0;
if (len > VM_MEMORY_SIZE) return 0;
return addr <= (vaddr_t)(VM_MEMORY_SIZE - len);
}
/**
* @brief Return a host C pointer into VM memory at @p addr.
*
* Validates the address with @c vm_addr_ok() (minimum 1-byte range) and
* returns @c NULL if the address is out of bounds or @p vm is uninitialised.
* The returned pointer is valid for at least one byte; callers that need
* multi-byte access should perform their own range check first.
*
* @param vm VM instance.
* @param addr VM byte offset to dereference.
* @return Host pointer into @c vm->memory, or NULL on bounds failure.
*/
uint8_t* vm_ptr(struct VM* vm, vaddr_t addr)
{
if (!vm || !vm->memory) return NULL;
if (!vm_addr_ok(vm, addr, 1)) return NULL;
return vm->memory + (size_t)addr;
}
/**
* @brief Load an unsigned byte from VM memory.
*
* Resolves @p addr via @c vm_ptr(). Sets @c vm->error and returns 0 if
* the address is out of bounds.
*
* @param vm VM instance.
* @param addr VM byte offset to read.
* @return Byte value at @p addr, or 0 on error.
*/
uint8_t vm_load_u8(struct VM* vm, vaddr_t addr)
{
uint8_t* p = vm_ptr(vm, addr);
if (!p)
{
vm->error = 1;
return 0;
}
return *p;
}
/**
* @brief Store an unsigned byte to VM memory.
*
* Resolves @p addr via @c vm_ptr(). Sets @c vm->error if the address
* is out of bounds; otherwise writes @p v.
*
* @param vm VM instance.
* @param addr VM byte offset to write.
* @param v Byte value to store.
*/
void vm_store_u8(struct VM* vm, vaddr_t addr, uint8_t v)
{
uint8_t* p = vm_ptr(vm, addr);
if (!p)
{
vm->error = 1;
return;
}
*p = v;
}
/**
* @brief Load an aligned cell from VM memory.
*
* Validates that @p addr is within bounds for a full @c cell_t read and
* that it is naturally aligned (addr % sizeof(cell_t) == 0). Uses
* @c memcpy() rather than a direct pointer dereference to avoid strict
* aliasing and alignment UB. Sets @c vm->error and returns 0 on failure.
*
* @param vm VM instance.
* @param addr Aligned VM byte offset to read.
* @return Cell value at @p addr, or 0 on alignment or bounds error.
*/
cell_t vm_load_cell(struct VM* vm, vaddr_t addr)
{
if (!vm_addr_ok(vm, addr, sizeof(cell_t)) || (addr % sizeof(cell_t)) != 0)
{
vm->error = 1;
return 0;
}
cell_t out = 0;
memcpy(&out, vm->memory + (size_t)addr, sizeof(cell_t));
return out;
}
/**
* @brief Store an aligned cell to VM memory.
*
* Validates alignment and bounds, then copies @p v into VM memory via
* @c memcpy(). Sets @c vm->error on alignment or bounds failure.
*
* @param vm VM instance.
* @param addr Aligned VM byte offset to write.
* @param v Cell value to store.
*/
void vm_store_cell(struct VM* vm, vaddr_t addr, cell_t v)
{
if (!vm_addr_ok(vm, addr, sizeof(cell_t)) || (addr % sizeof(cell_t)) != 0)
{
vm->error = 1;
return;
}
memcpy(vm->memory + (size_t)addr, &v, sizeof(cell_t));
}
/* vm_bootstrap_scr moved to vm_bootstrap.c */
/**
* @brief Mark the most recently created word as IMMEDIATE (FORTH-79).
*
* Sets @c WORD_IMMEDIATE on @c vm->latest so the word executes during
* compilation rather than being compiled into the current definition.
* Also calls @c physics_metadata_refresh_state() to update the physics
* engine's record of the entry's flags.
*
* Sets @c vm->error if @p vm has no @c latest entry (i.e., the dictionary
* is empty or no word has been created yet).
*
* @param vm VM instance.
*/
void vm_make_immediate(VM* vm)
{
if (!vm) return;
if (!vm->latest)
{
log_message(LOG_ERROR, "vm_make_immediate: no latest word to mark IMMEDIATE");
vm->error = 1;
return;
}
vm->latest->flags |= WORD_IMMEDIATE;
physics_metadata_refresh_state(vm->latest);
log_message(LOG_DEBUG, "IMMEDIATE: '%.*s'",
(int)vm->latest->name_len, vm->latest->name);
}
+243
View File
@@ -0,0 +1,243 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/vm_api.h"
#include "../include/vm.h" // internal details, not re-exposed to word modules
#include <string.h>
/** @name Stack Operations
* @{
*/
/** @brief Push value onto data stack
* @param vm VM instance
* @param v Value to push
*/
void vm_api_push(VM *vm, intptr_t v) { vm_push(vm, v); }
/** @brief Pop value from data stack
* @param vm VM instance
* @return Popped value
*/
intptr_t vm_api_pop(VM *vm) { return vm_pop(vm); }
/** @brief Push value onto return stack
* @param vm VM instance
* @param v Value to push
*/
void vm_api_rpush(VM *vm, intptr_t v) { vm_rpush(vm, v); }
/** @brief Pop value from return stack
* @param vm VM instance
* @return Popped value
*/
intptr_t vm_api_rpop(VM *vm) { return vm_rpop(vm); }
/** @} */
/** @name Dictionary Operations
* @{
*/
/** @brief Create new word in dictionary
* @param vm VM instance
* @param n Word name
* @param len Name length
* @param fn Word implementation function
* @return Pointer to created dictionary entry
*/
void *vm_api_create_word(VM *vm, const char *n, size_t len, word_func_t fn) {
return (void *) vm_create_word(vm, n, len, (void(*)(struct VM *)) fn);
}
/** @brief Find word in dictionary
* @param vm VM instance
* @param n Word name to find
* @param len Name length
* @return Pointer to found dictionary entry or NULL
*/
void *vm_api_find_word(VM *vm, const char *n, size_t len) {
return (void *) vm_find_word(vm, n, len);
}
/** @brief Hide last defined word
* @param vm VM instance
*/
void vm_api_hide_word(VM *vm) { vm_hide_word(vm); }
/** @brief Toggle smudge bit of last word
* @param vm VM instance
*/
void vm_api_smudge_word(VM *vm) { vm_smudge_word(vm); }
/** @} */
/** @brief Ensure input buffers are allocated
* @param vm VM instance
* @return 0 on success, 1 on error
* @details Allocates TIB buffer and input variables if not already allocated
*/
int vm_input_ensure(VM *vm) {
if (!vm) return 1;
if (!vm->tib_buf) {
vm->tib_buf = (unsigned char *) vm_allot(vm, INPUT_BUFFER_SIZE);
if (!vm->tib_buf) {
vm->error = 1;
return 1;
}
vm->tib_cap = INPUT_BUFFER_SIZE;
memset(vm->tib_buf, 0, vm->tib_cap);
}
if (!vm->in_var) {
/* INPUT_BUFFER_SIZE (1025) is not a cell multiple; align here->here before
* allocating cell-sized variables so their addresses pass the @ alignment check */
vm_align(vm);
vm->in_var = (cell_t *) vm_allot(vm, sizeof(cell_t));
if (!vm->in_var) {
vm->error = 1;
return 1;
}
*vm->in_var = 0;
}
if (!vm->span_var) {
vm->span_var = (cell_t *) vm_allot(vm, sizeof(cell_t));
if (!vm->span_var) {
vm->error = 1;
return 1;
}
*vm->span_var = 0;
}
return 0;
}
/** @brief Get pointer to terminal input buffer (TIB)
* @param vm VM instance
* @return Pointer to TIB or NULL on error
* @details Returns pointer to VM's terminal input buffer after ensuring it exists
*/
unsigned char *vm_input_tib(VM *vm) {
if (vm_input_ensure(vm)) return NULL;
return vm->tib_buf; /* VM address */
}
/** @brief Get pointer to input offset variable
* @param vm VM instance
* @return Pointer to input offset or NULL on error
* @details Returns pointer to VM's input offset variable after ensuring it exists
*/
cell_t *vm_input_in(VM *vm) {
if (vm_input_ensure(vm)) return NULL;
return vm->in_var; /* VM address */
}
/** @brief Get pointer to input span variable
* @param vm VM instance
* @return Pointer to span variable or NULL on error
* @details Returns pointer to VM's input span variable after ensuring it exists
*/
cell_t *vm_input_span(VM *vm) {
if (vm_input_ensure(vm)) return NULL;
return vm->span_var; /* VM address */
}
/** @brief Load input line into TIB
* @param vm VM instance
* @param src Source string to load
* @param n Number of characters to load
* @details Loads up to n characters from src into TIB, zero-pads remainder
* Resets input offset and updates span. Handles NULL src gracefully.
*/
void vm_input_load_line(VM *vm, const char *src, size_t n) {
if (vm_input_ensure(vm)) return;
if (!src) {
*vm->span_var = 0;
*vm->in_var = 0;
return;
}
size_t nclamp = n;
if (nclamp > vm->tib_cap) nclamp = vm->tib_cap;
memcpy(vm->tib_buf, src, nclamp);
if (nclamp < vm->tib_cap) memset(vm->tib_buf + nclamp, 0, vm->tib_cap - nclamp);
*vm->span_var = (cell_t) nclamp;
*vm->in_var = 0;
}
/** @brief Get current input source buffer info
* @param vm VM instance
* @param out_addr Output parameter for buffer address
* @param out_len Output parameter for buffer length
* @details Returns current input buffer address and length through parameters
* Both parameters are optional (can be NULL)
*/
void vm_input_source(VM *vm, cell_t *out_addr, cell_t *out_len) {
if (vm_input_ensure(vm)) {
if (out_addr) *out_addr = 0;
if (out_len) *out_len = 0;
return;
}
if (out_addr) *out_addr = (cell_t)(uintptr_t)
vm->tib_buf;
if (out_len) *out_len = (*vm->span_var < 0) ? 0 : *vm->span_var;
}
/** @name Memory Operations
* @{
*/
/** @brief Convert native pointer to Forth memory address
* @param vm VM instance
* @param p Native pointer to convert
* @return Forth memory address (offset from VM memory base)
* @details Converts a native C pointer to a Forth memory address by calculating
* offset from VM memory base. Returns 0 for invalid pointers.
*/
cell_t vm_addr_from_ptr(VM *vm, void *p) {
if (!vm || !p) return 0;
#if defined(VM_STRICT_PTR) && VM_STRICT_PTR
/* Forth addresses are real pointers */
return (cell_t)(uintptr_t)p;
#else
/* Forth addresses are offsets into vm->memory */
if (!vm->memory) return 0;
unsigned long base = (unsigned long) vm->memory;
unsigned long addr = (unsigned long) p;
if (addr < base) return 0;
return (cell_t)(addr - base);
#endif
}
/** @} */
+430
View File
@@ -0,0 +1,430 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* vm_bootstrap.c - How a VM comes into existence
*
* Contains:
* - dictionary seeding
* - primitive registration
* - initial vocabularies
* - host vs kernel bootstrap branching
* - early init code paths
*
* Rule: If it runs before the VM executes a single word, it belongs here.
*/
#include "vm_internal.h"
#include "../include/log.h"
#include "../include/word_registry.h"
#include "../include/vm_debug.h"
#include "../include/physics_hotwords_cache.h"
#include "../include/rolling_window_of_truth.h"
#include "../include/dictionary_heat_optimization.h"
#include "../include/ssm_jacquard.h"
#include "../include/platform_alloc.h"
#include "word_source/include/vocabulary_words.h"
#ifdef __STARKERNEL__
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_run.h"
#endif
#include <string.h>
/* ====================== Bootstrap helpers ======================= */
/**
* @brief Ensure the SCR (Screen) system variable cell is allocated and zeroed.
*
* If @c vm->scr_addr does not point to a valid, aligned cell inside the VM
* memory array, aligns the allocation pointer and allots a new cell.
* Always writes 0 to the SCR cell on exit. Called at the end of @c vm_init()
* to guarantee a clean SCR state regardless of how the initial allot
* sequence progressed.
*
* @param vm Active VM; sets @c vm->error = 1 and logs if allot fails
*/
static void vm_bootstrap_scr(VM* vm)
{
if (!vm) return;
/* Ensure SCR cell exists and is zero */
if (!vm_addr_ok(vm, vm->scr_addr, sizeof(cell_t)) ||
(vm->scr_addr % sizeof(cell_t)) != 0)
{
vm_align(vm);
void* p = vm_allot(vm, sizeof(cell_t));
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "bootstrap SCR allot failed");
return;
}
vm->scr_addr = (vaddr_t)((uint8_t*)p - vm->memory);
}
vm_store_cell(vm, vm->scr_addr, 0);
}
/**
* @brief Bootstrap a root vocabulary for the VM
*
* Creates a vocabulary by name, finds its dictionary entry, executes
* it to make it current, then calls DEFINITIONS.
*
* @param vm Pointer to VM structure
* @param name Vocabulary name to bootstrap
*/
void vm_bootstrap_root_vocabulary(VM *vm, const char *name)
{
if (!vm || !name) {
return;
}
vocabulary_create_vocabulary_direct(vm, name);
size_t len = strlen(name);
DictEntry *entry = vm_find_word(vm, name, len);
if (!entry || !entry->func) {
vm->error = 1;
log_message(LOG_ERROR, "bootstrap: missing vocabulary '%s'", name);
return;
}
entry->func(vm);
vocabulary_word_definitions(vm);
}
/* ====================== VM init / teardown ======================= */
/**
* @brief Initialize a new virtual machine instance
*
* Allocates memory and initializes all VM structures including:
* - Memory array
* - Data and return stacks
* - System variables (SCR, STATE, BASE)
* - Dictionary
* - Forth-79 wordset
*
* @param vm Pointer to VM structure to initialize
*/
void vm_init(VM* vm)
{
if (!vm) return;
memset(vm, 0, sizeof(*vm));
vm->next_word_id = 0;
vm->recycled_word_id_count = 0;
if (sf_mutex_init(&vm->dict_lock) != 0)
{
log_message(LOG_ERROR, "vm_init: dict_lock init failed");
vm->error = 1;
return;
}
if (sf_mutex_init(&vm->tuning_lock) != 0)
{
log_message(LOG_ERROR, "vm_init: tuning_lock init failed");
sf_mutex_destroy(&vm->dict_lock);
vm->error = 1;
return;
}
vm->memory = (uint8_t*)sf_malloc(VM_MEMORY_SIZE);
if (!vm->memory)
{
log_message(LOG_ERROR, "vm_init: out of memory");
vm->error = 1;
return;
}
vm->dsp = -1;
vm->rsp = -1;
vm->here = 0;
vm->exit_colon = 0;
/* Control-flow bookkeeping sentinels (-1/-999 mean "empty"; memset above
* already zeroed the arrays themselves) */
vm->cf.cf_sp = -1;
vm->cf.cf_last_mode = -999;
vm->cf.leave_sp = -1;
vm->cf.leave_mark_sp = -1;
vm->cf.endof_sp = -1;
vm->cf.endof_mark_sp = -1;
vm->abort_requested = 0;
vm_align(vm);
/* SCR */
{
void* p = vm_allot(vm, sizeof(cell_t));
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_init: SCR allot failed");
return;
}
vm->scr_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm_store_cell(vm, vm->scr_addr, 0);
}
/* STATE (0=interpret, -1=compile) */
{
void* p = vm_allot(vm, sizeof(cell_t));
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_init: STATE allot failed");
return;
}
vm->state_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm_store_cell(vm, vm->state_addr, 0);
vm->state_var = 0;
}
/* BASE (default 10) */
{
void* p = vm_allot(vm, sizeof(cell_t));
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_init: BASE allot failed");
return;
}
vm->base_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm_set_base(vm, 10);
}
/* Pictured-number hold buffer (64 bytes, per-VM so child VMs are independent) */
{
void* p = vm_allot(vm, 64);
if (!p)
{
vm->error = 1;
log_message(LOG_ERROR, "vm_init: hold buffer allot failed");
return;
}
vm->hold_addr = (vaddr_t)((uint8_t*)p - vm->memory);
vm->hold_pos = 0;
memset(p, 0, 64);
}
vm_bootstrap_scr(vm);
vm->mode = MODE_INTERPRET;
vm->compiling_word = NULL;
vm->latest = NULL;
vm->error = 0;
vm->halted = 0;
vm->input_length = 0;
vm->input_pos = 0;
vm->current_executing_entry = NULL;
vm_debug_set_current_vm(vm);
vm_debug_install_signal_handlers();
/* Register Forth-79 wordset */
register_forth79_words(vm);
/* Set FORGET fence to post-boot */
vm->dict_fence_latest = vm->latest;
vm->dict_fence_here = vm->here;
/* Initialize hot-words cache (physics frequency-driven acceleration) */
vm->hotwords_cache = (HotwordsCache*)sf_malloc(sizeof(HotwordsCache));
if (!vm->hotwords_cache)
{
log_message(LOG_ERROR, "vm_init: hotwords cache alloc failed");
vm->error = 1;
return;
}
hotwords_cache_init(vm->hotwords_cache);
/* Initialize rolling window of truth (deterministic execution history) */
if (rolling_window_init(&vm->rolling_window) != 0)
{
log_message(LOG_ERROR, "vm_init: rolling window alloc failed");
vm->error = 1;
return;
}
/* Initialize VM heartbeat (centralized time-driven tuning) */
vm->heartbeat.tick_count = 0;
vm->heartbeat.last_inference_tick = 0;
vm->heartbeat.check_counter = 0;
vm->heartbeat.heartbeat_enabled = 1; /* Enabled by default */
vm->heartbeat.tick_target_ns = HEARTBEAT_TICK_NS;
vm->heartbeat.snapshot_index = 0;
vm->heartbeat.worker = NULL;
vm->heartbeat_decay_cursor_id = WORD_ID_INVALID;
/* Initialize pipelining global metrics (aggregated prefetch tracking) */
vm->pipeline_metrics.prefetch_attempts = 0;
vm->pipeline_metrics.prefetch_hits = 0;
vm->pipeline_metrics.window_tuning_checks = 0;
vm->pipeline_metrics.last_checked_window_size = vm->rolling_window.effective_window_size;
vm->pipeline_metrics.last_checked_accuracy = 0.0;
vm->pipeline_metrics.suggested_next_size = vm->rolling_window.effective_window_size;
vm_heartbeat_publish_snapshot(vm);
/* Start heartbeat thread (if enabled) */
vm_heartbeat_start_thread(vm);
/* Initialize adaptive heat decay tuning (Loop #3) */
/* Start with 2:1 ratio in Q48.16 format: 2.0 << 16 = 131072 */
vm->decay_slope_q48 = (1ULL << 16) / 3; /* 1/3 starting slope (Q48.16) */
vm->last_decay_check_ns = 0;
vm->total_heat_at_last_check = 0;
vm->stale_word_count_at_check = 0;
vm->decay_slope_direction = 0; /* Start neutral */
/* Phase 2: Initialize heat-aware dictionary optimization */
vm->lookup_strategy = 0; /* Start with naive lookup, will adapt based on patterns */
vm->last_bucket_reorg_ns = 0; /* Force first reorg quickly */
dict_update_heat_percentiles(vm); /* Calculate initial percentiles */
/* SSM L8: Jacquard Mode Selector initialization */
vm->ssm_l8_state = sf_malloc(sizeof(ssm_l8_state_t));
if (!vm->ssm_l8_state)
{
log_message(LOG_ERROR, "vm_init: SSM L8 state alloc failed");
vm->error = 1;
return;
}
ssm_l8_init((ssm_l8_state_t*)vm->ssm_l8_state, SSM_MODE_C0);
ssm_l8_init_table((ssm_l8_state_t*)vm->ssm_l8_state);
vm->ssm_config = sf_malloc(sizeof(ssm_config_t));
if (!vm->ssm_config)
{
log_message(LOG_ERROR, "vm_init: SSM config alloc failed");
vm->error = 1;
return;
}
/* Initialize with C0 (minimal) mode: all 7 loops off. The adaptive
* table's first trial-end (or an explicit ssm_apply_mode_from_table()
* call) overwrites this with the real DoE-seeded starting config. */
((ssm_config_t*)vm->ssm_config)->L1_heat_tracking = 0;
((ssm_config_t*)vm->ssm_config)->L2_rolling_window = 0;
((ssm_config_t*)vm->ssm_config)->L3_linear_decay = 0;
((ssm_config_t*)vm->ssm_config)->L4_pipelining = 0;
((ssm_config_t*)vm->ssm_config)->L5_window_inference = 0;
((ssm_config_t*)vm->ssm_config)->L6_decay_inference = 0;
((ssm_config_t*)vm->ssm_config)->L7_adaptive_heartrate = 0;
#ifdef __STARKERNEL__
/* Capsule subsystem: register VM hooks, init registry and run log.
* capsule_birth_mama() is called separately after vm_init() returns,
* once the caller has access to the compiled-in capsule directory. */
capsule_vm_hooks_register();
capsule_vm_registry_init();
capsule_run_log_init();
#endif
}
/**
* @brief Clean up and free VM resources
*
* Frees allocated memory and resets VM state.
*
* @param vm Pointer to VM structure to clean up
*/
void vm_cleanup(VM* vm)
{
if (!vm) return;
/* Stop heartbeat thread (if running) */
vm_heartbeat_stop_thread(vm);
/* Clean up hot-words cache */
if (vm->hotwords_cache)
{
hotwords_cache_cleanup(vm->hotwords_cache);
sf_free(vm->hotwords_cache);
vm->hotwords_cache = NULL;
}
/* Clean up rolling window of truth */
rolling_window_cleanup(&vm->rolling_window);
/* Clean up SSM L8 state */
if (vm->ssm_l8_state)
{
ssm_l8_free_table((ssm_l8_state_t*)vm->ssm_l8_state);
sf_free(vm->ssm_l8_state);
vm->ssm_l8_state = NULL;
}
if (vm->ssm_config)
{
sf_free(vm->ssm_config);
vm->ssm_config = NULL;
}
/* Same fix as the kernel's own vm_cleanup() (src/starkernel/vm/vm_core.c)
* -- every DictEntry is its own sf_malloc() (vm_create_word(),
* dictionary_management.c), separate from vm->memory entirely, and
* nothing anywhere ever freed them. Confirmed live via a kernel-side
* repeated birth/kill cycle exhausting the heap; this hosted copy of
* vm_cleanup() has the identical gap, just never noticed here because
* the hosted binary normally only calls this once at process exit. */
{
DictEntry *dict_entry = vm->latest;
while (dict_entry) {
DictEntry *next_entry = dict_entry->link;
if (dict_entry->transition_metrics) {
sf_free(dict_entry->transition_metrics);
}
sf_free(dict_entry);
dict_entry = next_entry;
}
vm->latest = NULL;
}
if (vm->memory)
{
sf_free(vm->memory);
vm->memory = NULL;
}
vm->here = 0;
sf_mutex_destroy(&vm->tuning_lock);
sf_mutex_destroy(&vm->dict_lock);
}
+205
View File
@@ -0,0 +1,205 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/vm.h"
#include "../include/vm_debug.h"
/* No POSIX signals in the kernel — provide stubs and skip the rest. */
#if defined(STARFORTH_MINIMAL)
void vm_debug_set_current_vm(VM *vm) { (void)vm; }
void vm_debug_dump_state(const VM *vm, const char *reason) { (void)vm; (void)reason; }
void vm_debug_install_signal_handlers(void) {}
#else
#include <signal.h>
#include <stdio.h>
#include <string.h>
static VM *g_vm = NULL;
/* @brief Sets the current VM instance for debug operations
* @param vm Pointer to the VM instance to be monitored
*/
void vm_debug_set_current_vm(VM *vm) { g_vm = vm; }
/* Safe BASE read (never touches vm->error). Adjust if you have a helper. */
/** @brief Safely retrieves the current number base from VM
* @param vm Pointer to the VM instance
* @return Current number base (2-36) or 10 if invalid
*/
static unsigned dbg_get_base(const VM *vm) {
if (!vm) return 10u;
size_t idx = (size_t) vm->base_addr;
if (idx < (size_t) VM_MEMORY_SIZE) {
cell_t v = vm->memory[idx];
if (v >= 2 && v <= 36) return (unsigned) v;
}
if (vm->base >= 2 && vm->base <= 36) return (unsigned) vm->base;
return 10u;
}
/** @brief Converts VM mode to string representation
* @param m The VM mode value
* @return String representing the mode
*/
static const char *mode_str(int m) {
switch (m) {
case MODE_INTERPRET: return "INTERPRET";
case MODE_COMPILE: return "COMPILE";
default: return "UNKNOWN";
}
}
/** @brief Dumps preview of a block's contents
* @param vm Pointer to the VM instance
* @param scr Block number to preview
*/
static void dump_block_preview(const VM *vm, cell_t scr) {
if (!vm) return;
if (scr < 1 || scr >= (cell_t) MAX_BLOCKS) return;
vaddr_t addr = (vaddr_t)((unsigned long long) scr * (unsigned long long) BLOCK_SIZE);
if (!vm_addr_ok((VM *) vm, addr, BLOCK_SIZE)) return;
const unsigned char *p = vm_ptr((VM *) vm, addr);
char line[81];
size_t n = 0;
while (n < 80 && n < BLOCK_SIZE) {
unsigned char ch = p[n];
if (ch == 0 || ch == '\n' || ch == '\r') break;
line[n++] = (ch < 32 || ch > 126) ? ' ' : (char) ch;
}
line[n] = '\0';
fprintf(stderr, " block[%ld]: \"%s\"%s\n", (long) scr, line, (n == 80 ? "..." : ""));
}
/* @brief Dumps the current state of the VM for debugging
* @param vm Pointer to the VM instance to dump
* @param reason Optional string describing the reason for the dump
*/
void vm_debug_dump_state(const VM *vm, const char *reason) {
if (!vm) {
fprintf(stderr, "===== VM STATE DUMP (no VM) [%s] =====\n", reason ? reason : "");
return;
}
unsigned base = dbg_get_base(vm);
cell_t scr = 0;
if ((size_t) vm->scr_addr < (size_t) VM_MEMORY_SIZE) scr = vm->memory[(size_t) vm->scr_addr];
fprintf(stderr, "\n===== VM STATE DUMP ===== [%s]\n", reason ? reason : "");
fprintf(stderr, " mode=%s error=%d\n", mode_str(vm->mode), vm->error);
fprintf(stderr, " HERE=%ld BASE=%u SCR=%ld\n", (long) vm->here, base, (long) scr);
fprintf(stderr, " dsp=%d rsp=%d\n", vm->dsp, vm->rsp);
/* If your VM struct exposes stacks, show top few items (guarded). */
#ifdef DATA_STACK_SIZE
if (vm->stack) {
fprintf(stderr, " DATA(top=%d):", vm->dsp);
int shown = 0;
for (int i = vm->dsp; i >= 0 && shown < 16; --i, ++shown)
fprintf(
stderr, " [%d]=%ld", i, (long) vm->stack[i]);
if (vm->dsp >= 16) fprintf(stderr, " ...");
fprintf(stderr, "\n");
}
#endif
#ifdef RETURN_STACK_SIZE
if (vm->rstack) {
fprintf(stderr, " RETURN(top=%d):", vm->rsp);
int shown = 0;
for (int i = vm->rsp; i >= 0 && shown < 16; --i, ++shown)
fprintf(
stderr, " [%d]=%ld", i, (long) vm->rstack[i]);
if (vm->rsp >= 16) fprintf(stderr, " ...");
fprintf(stderr, "\n");
}
#endif
/* If you track current word/ip, print them (guard with your flags/macros). */
#ifdef VM_HAS_CURRENT_ENTRY
if (vm->current_executing_entry) fprintf(stderr, " current=%s\n", vm->current_executing_entry->name);
#endif
#ifdef VM_HAS_IP
fprintf(stderr, " ip=%ld\n", (long) vm->ip);
#endif
dump_block_preview(vm, scr);
fprintf(stderr, "===== END VM STATE DUMP =====\n");
}
/** @brief Signal handler for crash debugging
* @param sig Signal number that was caught
*/
static void sig_handler(int sig) {
const char *name = (sig == SIGSEGV) ? "SIGSEGV" : (sig == SIGABRT) ? "SIGABRT" : "SIGNAL";
fprintf(stderr, "\n*** Caught %s ***\n", name);
if (g_vm) vm_debug_dump_state(g_vm, name);
signal(sig, SIG_DFL);
raise(sig);
}
/** @brief Installs signal handlers for crash debugging
* @details Sets up handlers for SIGSEGV and SIGABRT to dump VM state on crash
*/
void vm_debug_install_signal_handlers(void) {
#if defined(__unix__) || defined(__APPLE__)
/* Use sigaction when available */
#ifdef SA_RESETHAND
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sig_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESETHAND; /* one-shot: reset to default after firing */
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
#else
/* Fallback: plain signal() if SA_RESETHAND isn’t defined */
signal(SIGSEGV, sig_handler);
signal(SIGABRT, sig_handler);
#endif
#else
/* Non-POSIX platforms: best-effort */
signal(SIGSEGV, sig_handler);
signal(SIGABRT, sig_handler);
#endif
}
#endif /* !STARFORTH_MINIMAL */
+126
View File
@@ -0,0 +1,126 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef STARFORTH_VM_INTERNAL_H
#define STARFORTH_VM_INTERNAL_H
/*
* vm_internal.h - Shared private declarations for VM implementation
*
* This header is ONLY included by VM .c files (vm.c, vm_time.c, vm_bootstrap.c).
* NOT part of the public API.
*/
#include "../include/vm.h"
#include "../include/platform_time.h"
#include <stdint.h>
/* HISTORICAL: this condition also excluded L4RE_TARGET (`&&
* !defined(L4RE_TARGET)`) -- L4Re had no pthreads. L4Re support has been
* removed as an active target. */
#if HEARTBEAT_THREAD_ENABLED
#include <pthread.h>
#define HEARTBEAT_HAS_THREADS 1
#else
#define HEARTBEAT_HAS_THREADS 0
#endif
/* Heartbeat decay batch size */
#define HEARTBEAT_DECAY_BATCH 64u
/* HeartbeatWorker - background thread state for heartbeat */
typedef struct HeartbeatWorker
{
#if HEARTBEAT_HAS_THREADS
pthread_t thread;
#endif
uint64_t tick_ns;
int running;
int stop_requested;
} HeartbeatWorker;
/* ====================== Atomic helpers ======================= */
static inline uint32_t heartbeat_snapshot_index_load(const volatile uint32_t *ptr)
{
#if defined(__GNUC__)
return __atomic_load_n(ptr, __ATOMIC_ACQUIRE);
#else
return *ptr;
#endif
}
static inline void heartbeat_snapshot_index_store(volatile uint32_t *ptr, uint32_t value)
{
#if defined(__GNUC__)
__atomic_store_n(ptr, value, __ATOMIC_RELEASE);
#else
*ptr = value;
#endif
}
/* ====================== Cross-file function declarations ======================= */
/* vm.c (core) - execution spine */
void execute_colon_word(VM *vm);
/* vm_time.c - heartbeat/physics */
void vm_tick(VM *vm);
void vm_tick_window_tuner(VM *vm);
void vm_tick_slope_validator(VM *vm);
void vm_tick_inference_engine(VM *vm);
void vm_heartbeat_run_cycle(VM *vm);
void vm_snapshot_read(const VM *vm, HeartbeatSnapshot *out_snapshot);
void vm_heartbeat_start_thread(VM *vm);
void vm_heartbeat_stop_thread(VM *vm);
void vm_heartbeat_publish_snapshot(VM *vm);
/* vm_bootstrap.c - initialization */
void vm_init(VM *vm);
void vm_cleanup(VM *vm);
/* vm.c (core) - base helpers used by bootstrap */
unsigned vm_get_base(const VM *vm);
void vm_set_base(VM *vm, unsigned b);
#endif /* STARFORTH_VM_INTERNAL_H */
+796
View File
@@ -0,0 +1,796 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* vm_time.c - Heartbeat, timers, entropy/heat tracking, decay
*
* Time is a first-class liar — isolated here to keep the James Law
* machinery from infecting everything else.
*
* Contains:
* - heartbeat logic
* - timers
* - entropy / heat tracking
* - decay / accumulation
* - any "tick" based logic
*/
#include "vm_internal.h"
#include "../include/inference_engine.h"
#include "../include/log.h"
#include "../include/physics_metadata.h"
#include "../include/physics_hotwords_cache.h"
#include "../include/rolling_window_of_truth.h"
#include "../include/dictionary_heat_optimization.h"
#include "../include/ssm_jacquard.h"
#include "../include/platform_alloc.h"
#include <string.h>
#include <errno.h>
#include <time.h>
/* ====================== Forward Declarations ======================= */
static void vm_tick_apply_background_decay(VM *vm, uint64_t now_ns);
static void vm_heartbeat_update_l8(VM *vm);
#if HEARTBEAT_THREAD_ENABLED
static void* heartbeat_thread_main(void *arg);
#endif
/* ====================== Snapshot Publishing ======================= */
void vm_heartbeat_publish_snapshot(VM *vm)
{
if (!vm)
return;
uint32_t current = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
uint32_t next = current ^ 1u;
HeartbeatSnapshot *snapshot = &vm->heartbeat.snapshots[next];
snapshot->published_tick = vm->heartbeat.tick_count;
snapshot->published_ns = sf_monotonic_ns();
snapshot->window_width = vm->rolling_window.effective_window_size;
snapshot->actual_window_size = (uint32_t)(vm->rolling_window.total_executions < ROLLING_WINDOW_SIZE
? vm->rolling_window.total_executions
: ROLLING_WINDOW_SIZE);
snapshot->decay_slope_q48 = vm->decay_slope_q48;
snapshot->hot_word_count = vm->hot_word_count_at_check;
snapshot->stale_word_count = vm->stale_word_count_at_check;
snapshot->total_heat = vm->total_heat_at_last_check;
heartbeat_snapshot_index_store(&vm->heartbeat.snapshot_index, next);
}
/* ====================== VM Heartbeat (Time-Driven Tuning) ======================= */
/**
* @brief Central heartbeat dispatcher for time-driven tuning operations
*
* Aggregates all periodic optimization tasks (Loop #3 and Loop #5) into one place.
* Designed as plugin architecture - new tuning operations can be added as plugins.
*
* Options for integration:
* - Synchronous (now): Called from main execution loop, every N executions
* - Background thread (future): Runs in separate thread, decoupled from VM execution
*
* @param vm Pointer to VM instance
*/
void vm_tick(VM* vm)
{
if (!vm || !vm->heartbeat.heartbeat_enabled)
return;
vm->heartbeat.tick_count++;
/* Unified Inference Engine (Phase 2: Replaces Loops #3 & #5)
* Runs every HEARTBEAT_INFERENCE_FREQUENCY ticks to infer optimal:
* - Window width (via variance inflection detection)
* - Decay slope (via exponential regression on heat trajectory)
*/
if ((vm->heartbeat.tick_count - vm->heartbeat.last_inference_tick) >= HEARTBEAT_INFERENCE_FREQUENCY)
{
vm_tick_inference_engine(vm);
}
/* Plugin 2: System State Monitoring (Future) */
/* vm_tick_system_monitor(vm); */
/* Plugin 3: Formal Verification State Update (Future) */
/* vm_tick_formal_state_sync(vm); */
}
/**
* @brief Loop #5: Context-aware window tuning via binary chop search
*
* Uses prefetch accuracy to guide window size adaptation.
* Binary search converges on optimal effective_window_size for current workload.
*
* @param vm Pointer to VM instance
*/
void vm_tick_window_tuner(VM* vm)
{
if (!vm || !vm->rolling_window.is_warm || !ENABLE_PIPELINING)
return;
RollingWindowOfTruth *window = &vm->rolling_window;
PipelineGlobalMetrics *metrics = &vm->pipeline_metrics;
/* Calculate current prefetch accuracy */
if (metrics->prefetch_attempts == 0)
return; /* Not enough data yet */
double current_accuracy = (double)metrics->prefetch_hits / (double)metrics->prefetch_attempts;
/* Binary chop suggests next window size to try */
uint32_t suggested_size = window->effective_window_size; /* Default: no change */
if (metrics->window_tuning_checks == 0)
{
/* First check: try shrinking by 25% */
suggested_size = (window->effective_window_size * 75) / 100;
}
else
{
/* Compare current accuracy to last check */
double accuracy_delta = current_accuracy - metrics->last_checked_accuracy;
if (accuracy_delta > 0.01) /* Improvement threshold: 1% */
{
/* Accuracy improved! Try shrinking more */
uint32_t smaller = (window->effective_window_size * 75) / 100;
suggested_size = (smaller > ADAPTIVE_MIN_WINDOW_SIZE) ? smaller : ADAPTIVE_MIN_WINDOW_SIZE;
}
else if (accuracy_delta < -0.01)
{
/* Accuracy degraded. Try growing instead */
uint32_t larger = (window->effective_window_size * 133) / 100; /* Grow by ~33% */
suggested_size = (larger < ROLLING_WINDOW_SIZE) ? larger : ROLLING_WINDOW_SIZE;
}
/* else: Plateau, stick with current size */
}
/* Apply if different */
if (suggested_size != window->effective_window_size)
{
log_message(LOG_DEBUG,
"HEARTBEAT[window]: %u → %u (accuracy %.2f%%, %lu/%lu prefetch hits)",
window->effective_window_size,
suggested_size,
current_accuracy * 100.0,
metrics->prefetch_hits,
metrics->prefetch_attempts);
window->effective_window_size = suggested_size;
}
/* Record for next iteration */
metrics->last_checked_window_size = window->effective_window_size;
metrics->last_checked_accuracy = current_accuracy;
metrics->window_tuning_checks++;
}
/**
* @brief Loop #3: Heat decay slope validation via periodic measurement
*
* Validates that linear decay is actually helping optimize dictionary caching.
* Measures stale word ratio, hot word count, and average heat distribution.
*
* @param vm Pointer to VM instance
*/
void vm_tick_slope_validator(VM* vm)
{
if (!vm)
return;
/* Collect snapshot of current state */
uint64_t hot_word_count = 0;
uint64_t stale_word_count = 0;
uint64_t total_heat = 0;
uint32_t word_count = 0;
/* Scan dictionary and categorize words by heat level */
sf_mutex_lock(&vm->dict_lock);
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
{
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
hot_word_count++;
else if (e->execution_heat > 0 && e->execution_heat < 10)
stale_word_count++;
total_heat += e->execution_heat;
word_count++;
}
sf_mutex_unlock(&vm->dict_lock);
double avg_heat = (word_count > 0) ? (double)total_heat / (double)word_count : 0.0;
double stale_ratio = (word_count > 0) ? (double)stale_word_count / (double)word_count : 0.0;
/* === LOOP #3: INFERENCE ENGINE ===
* Compare current measurements to baseline from last check
* Decide whether decay is too fast, too slow, or optimal
*/
int new_slope_direction = 0; /* -1: decrease slope, 0: stable, +1: increase slope */
sf_mutex_lock(&vm->tuning_lock);
if (vm->word_count_at_check > 0)
{
/* Calculate trend in stale words: absolute count delta indicates accumulation/clearing */
int64_t stale_delta = (int64_t)stale_word_count - (int64_t)vm->stale_word_count_at_check;
/* INFERENCE: If stale words INCREASING, decay is too slow → increase slope */
/* If stale words DECREASING, decay is working (or too fast) → monitor */
if (stale_delta > 5) /* Threshold: 5+ additional stale words signals problem */
{
/* Stale words accumulating: decay is insufficient */
new_slope_direction = 1;
log_message(LOG_DEBUG,
"HEARTBEAT[slope]: stale_delta=%ld, decay TOO SLOW, increase slope",
(long)stale_delta);
}
else if (stale_delta < -5) /* Threshold: 5+ fewer stale words signals clearing */
{
/* Stale words clearing: decay is aggressive (potentially too fast) */
/* Only decrease slope if avg_heat is dropping below target */
if (avg_heat < 5.0)
{
new_slope_direction = -1;
log_message(LOG_DEBUG,
"HEARTBEAT[slope]: stale_delta=%ld, avg_heat=%.1f, decay TOO FAST, decrease slope",
(long)stale_delta, avg_heat);
}
else
{
log_message(LOG_DEBUG,
"HEARTBEAT[slope]: stale_delta=%ld, decay working, hold slope",
(long)stale_delta);
}
}
else
{
log_message(LOG_DEBUG,
"HEARTBEAT[slope]: stale_delta=%ld (stable), hold slope",
(long)stale_delta);
}
}
else
{
log_message(LOG_DEBUG,
"HEARTBEAT[slope]: baseline measurement - hot_words=%lu, stale_ratio=%.2f%%, avg_heat=%.1f",
hot_word_count,
stale_ratio * 100.0,
avg_heat);
}
/* === APPLY SLOPE ADJUSTMENT ===
* Only adjust if direction changed (hysteresis to prevent oscillation)
*/
if (new_slope_direction != vm->decay_slope_direction && new_slope_direction != 0)
{
vm->decay_slope_direction = new_slope_direction;
/* Calculate adjustment in Q48.16: 5% change per cycle */
uint64_t adjustment = (vm->decay_slope_q48 * 5) / 100;
if (adjustment < 1) adjustment = 1; /* Minimum increment */
uint64_t old_slope = vm->decay_slope_q48;
if (new_slope_direction > 0)
{
vm->decay_slope_q48 += adjustment;
}
else if (new_slope_direction < 0)
{
vm->decay_slope_q48 = (vm->decay_slope_q48 > adjustment)
? (vm->decay_slope_q48 - adjustment)
: 1; /* Floor at 1 */
}
/* Log the adjustment as human-readable double */
double old_slope_dbl = (double)old_slope / 65536.0;
double new_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
log_message(LOG_DEBUG,
"HEARTBEAT[slope]: ADJUSTED slope from %.3f to %.3f (direction=%d)",
old_slope_dbl,
new_slope_dbl,
new_slope_direction);
}
/* Store baseline for next comparison */
vm->hot_word_count_at_check = hot_word_count;
vm->total_heat_at_last_check = total_heat;
vm->stale_word_count_at_check = stale_word_count;
vm->word_count_at_check = word_count;
sf_mutex_unlock(&vm->tuning_lock);
(void)stale_ratio; /* Suppress unused warning */
}
static void vm_tick_apply_background_decay(VM *vm, uint64_t now_ns)
{
if (!vm)
return;
sf_mutex_lock(&vm->dict_lock);
DictEntry *cursor = NULL;
if (vm->heartbeat_decay_cursor_id != WORD_ID_INVALID)
cursor = vm_dictionary_lookup_by_word_id(vm, vm->heartbeat_decay_cursor_id);
if (!cursor)
cursor = vm->latest;
size_t processed = 0;
while (cursor && processed < HEARTBEAT_DECAY_BATCH)
{
/* Tick-based, not wall-clock -- see physics_metadata_apply_linear_decay().
* now_ns is kept only to refresh last_decay_ns for diagnostics. */
uint64_t elapsed_ticks = vm->heartbeat.tick_count - cursor->physics.last_decay_tick;
physics_metadata_apply_linear_decay(cursor, elapsed_ticks, vm);
cursor->physics.last_decay_tick = vm->heartbeat.tick_count;
cursor->physics.last_decay_ns = now_ns;
cursor = cursor->link;
processed++;
}
vm->heartbeat_decay_cursor_id = (cursor && cursor->word_id != WORD_ID_INVALID)
? cursor->word_id
: WORD_ID_INVALID;
sf_mutex_unlock(&vm->dict_lock);
}
/**
* @brief L8 FINAL INTEGRATION: Jacquard mode selector heartbeat update
*
* Collects metrics from L1-L7 physics layers and feeds them to the L8 Jacquard
* mode selector. L8 then chooses the optimal configuration mode based on workload
* characteristics and applies it to the runtime.
*
* This is the SOLE policy engine - all adaptive decisions flow through L8.
*
* @param vm Pointer to VM instance
*/
static void vm_heartbeat_update_l8(VM *vm)
{
if (!vm || !vm->ssm_l8_state)
return;
ssm_l8_state_t *l8 = (ssm_l8_state_t*)vm->ssm_l8_state;
/* === Collect L1-L7 Metrics and Convert to L8 Format === */
ssm_l8_metrics_t metrics = {0};
/* L2: Rolling window entropy (normalized diversity) */
/* Use unique word count / total executions as entropy proxy */
uint32_t unique_words = (vm->rolling_window.total_executions > 0) ?
(uint32_t)(vm->rolling_window.effective_window_size) : 0;
metrics.entropy = (double)unique_words / (double)(ROLLING_WINDOW_SIZE > 0 ? ROLLING_WINDOW_SIZE : 1);
/* L4: Pipelining metrics → CV (coefficient of variation) */
if (vm->pipeline_metrics.prefetch_attempts > 0) {
double accuracy = (double)vm->pipeline_metrics.prefetch_hits / (double)vm->pipeline_metrics.prefetch_attempts;
metrics.cv = 1.0 - accuracy; /* Invert: high accuracy = low CV (stability) */
} else {
metrics.cv = 0.5; /* Default moderate CV if no data */
}
/* L3: Decay slope → temporal locality signal (Q48.16 → double) */
double decay_slope = (double)vm->decay_slope_q48 / 65536.0;
metrics.temporal_decay = (decay_slope > 0.0) ? (1.0 / decay_slope) : 0.0; /* Higher slope = stronger temporal locality */
if (metrics.temporal_decay > 1.0) metrics.temporal_decay = 1.0; /* Clamp to [0,1] */
/* L5/L6: Inference stability for hysteresis + table ANOVA tracking */
{
int early_exited;
sf_mutex_lock(&vm->tuning_lock);
early_exited = (vm->last_inference_outputs && vm->last_inference_outputs->early_exited);
sf_mutex_unlock(&vm->tuning_lock);
metrics.stability_score = early_exited ? 0.9 : 0.1;
metrics.inference_early_exited = early_exited;
/* Inference ran this tick if last_inference_tick == tick_count */
metrics.inference_ran_this_tick =
(vm->heartbeat.tick_count == vm->heartbeat.last_inference_tick) ? 1 : 0;
}
if (l8->table) {
/* === Adaptive table-based selection === */
uint8_t old_config = l8->table->current_config;
CDTuning cd_tuning = cd_tuning_word();
uint32_t recent_ids[CD_MAX_CLASSIFY_DEPTH];
uint32_t recent_count = rolling_window_get_recent_sequence(
&vm->rolling_window, cd_tuning.id_window_depth, recent_ids);
ssm_l8_update_table(l8, &metrics,
(ssm_config_t*)vm->ssm_config,
vm->rolling_window.effective_window_size,
recent_ids, recent_count);
if (l8->table->current_config != old_config) {
uint8_t bits = l8->table->entries[l8->table->current_config].config_bits;
ssm_config_t *cfg = (ssm_config_t*)vm->ssm_config;
log_message(LOG_DEBUG,
"L8[TABLE]: Config %u → %u regime=%u score=%u "
"L1=%d L2=%d L3=%d L4=%d L5=%d L6=%d L7=%d",
(unsigned)old_config,
(unsigned)l8->table->current_config,
(unsigned)l8->table->current_regime,
(unsigned)l8->table->regime_scores
[l8->table->current_regime * l8->table->num_configs
+ l8->table->current_config],
cfg->L1_heat_tracking, cfg->L2_rolling_window,
cfg->L3_linear_decay, cfg->L4_pipelining,
cfg->L5_window_inference, cfg->L6_decay_inference,
cfg->L7_adaptive_heartrate);
(void)bits;
}
} else {
/* === Legacy threshold classifier === */
ssm_l8_mode_t old_mode = l8->current_mode;
ssm_l8_update(&metrics, l8);
if (l8->current_mode != old_mode) {
ssm_config_t *config = (ssm_config_t*)vm->ssm_config;
ssm_apply_mode(l8, config);
log_message(LOG_DEBUG,
"L8[JACQUARD]: Mode %s → %s (entropy=%.2f, cv=%.2f, temporal=%.2f)",
ssm_l8_mode_name(old_mode),
ssm_l8_mode_name(l8->current_mode),
metrics.entropy,
metrics.cv,
metrics.temporal_decay);
}
}
/* L1's cache->enabled is the runtime toggle physics_pre_execute's
* hotwords-cache checks actually read (see physics_execution_hooks.c);
* sync it here rather than re-deriving ssm_config on the hot dispatch
* path every word execution. Idempotent -- cheap even when unchanged. */
if (vm->hotwords_cache && vm->ssm_config) {
hotwords_cache_set_enabled(vm->hotwords_cache,
((ssm_config_t*)vm->ssm_config)->L1_heat_tracking);
}
}
void vm_heartbeat_run_cycle(VM *vm)
{
if (!vm || !vm->heartbeat.heartbeat_enabled)
return;
/* L8 FINAL INTEGRATION: Core heartbeat operations */
vm_tick(vm);
vm_tick_apply_background_decay(vm, sf_monotonic_ns());
rolling_window_service(&vm->rolling_window);
dict_adaptive_optimization_pass(vm); /* Adaptive dictionary optimization */
/* L8 FINAL INTEGRATION: Jacquard mode selector - the sole policy engine */
vm_heartbeat_update_l8(vm);
vm_heartbeat_publish_snapshot(vm);
/* Phase 2: Real-time heartbeat metrics emission (DISABLED)
* Re-enable with --heartbeat-log=full or add a runtime flag.
*/
#if 0
HeartbeatTickSnapshot tick_snapshot;
heartbeat_capture_tick_snapshot(vm, &tick_snapshot);
heartbeat_emit_tick_row(vm, &tick_snapshot);
#endif
}
#if HEARTBEAT_THREAD_ENABLED
static void* heartbeat_thread_main(void *arg)
{
VM *vm = (VM*)arg;
if (!vm || !vm->heartbeat.worker)
return NULL;
HeartbeatWorker *worker = vm->heartbeat.worker;
worker->running = 1;
/* IMPORTANT: No startup delay to allow heartbeat to emit during short DoE runs.
* Original 50ms delay avoided race conditions during word registration, but prevented
* real-time metrics emission in fast-completing tests. */
while (!worker->stop_requested)
{
vm_heartbeat_run_cycle(vm);
uint64_t tick_ns = worker->tick_ns ? worker->tick_ns : HEARTBEAT_TICK_NS;
struct timespec req;
req.tv_sec = (time_t)(tick_ns / 1000000000ULL);
req.tv_nsec = (long)(tick_ns % 1000000000ULL);
while (!worker->stop_requested && nanosleep(&req, &req) == -1 && errno == EINTR)
{
/* Retry with remaining time */
}
}
worker->running = 0;
return NULL;
}
#endif
void vm_snapshot_read(const VM* vm, HeartbeatSnapshot* out_snapshot)
{
if (!vm || !out_snapshot)
return;
uint32_t index = heartbeat_snapshot_index_load(&vm->heartbeat.snapshot_index) & 1u;
*out_snapshot = vm->heartbeat.snapshots[index];
}
/**
* @brief Phase 2: Unified Inference Engine - Adaptive Window & Decay Slope Tuning
*
* Coordinates inference on rolling window of truth to determine:
* - Optimal adaptive window width (via variance inflection detection)
* - Optimal decay slope (via exponential regression on heat trajectory)
*
* Uses ANOVA early-exit to skip full inference when variance is stable (<5% change).
* All math uses Q48.16 fixed-point (integer-only, no floating-point).
*
* Replaces legacy vm_tick_window_tuner() and vm_tick_slope_validator().
*
* @param vm Pointer to VM instance
*/
void vm_tick_inference_engine(VM* vm)
{
if (!vm || !vm->heartbeat.heartbeat_enabled || !vm->rolling_window.is_warm)
return;
/* DoE counter: inference engine invocations */
vm->heartbeat.inference_run_count++;
rolling_window_service(&vm->rolling_window);
/* Allocate InferenceOutputs if needed - protect with tuning_lock against race with doe_metrics */
sf_mutex_lock(&vm->tuning_lock);
if (!vm->last_inference_outputs)
{
vm->last_inference_outputs = sf_malloc(sizeof(InferenceOutputs));
if (!vm->last_inference_outputs)
{
sf_mutex_unlock(&vm->tuning_lock);
log_message(LOG_ERROR, "INFERENCE: Failed to allocate InferenceOutputs");
return;
}
memset(vm->last_inference_outputs, 0, sizeof(InferenceOutputs));
}
sf_mutex_unlock(&vm->tuning_lock);
/* === Collect Current Dictionary Metrics === */
uint64_t hot_word_count = 0;
uint64_t stale_word_count = 0;
uint64_t total_heat = 0;
uint32_t word_count = 0;
sf_mutex_lock(&vm->dict_lock);
for (DictEntry *e = vm->latest; e != NULL; e = e->link)
{
if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD)
hot_word_count++;
else if (e->execution_heat > 0 && e->execution_heat < 10)
stale_word_count++;
total_heat += e->execution_heat;
word_count++;
}
sf_mutex_unlock(&vm->dict_lock);
/* === Populate InferenceInputs === */
InferenceInputs inference_inputs = {
.window = &vm->rolling_window,
.vm = vm, /* Required for dictionary lookups in extract_heat_trajectory */
.trajectory_length = (vm->rolling_window.window_pos > 0)
? vm->rolling_window.window_pos
: vm->rolling_window.total_executions,
.prefetch_hits = vm->pipeline_metrics.prefetch_hits,
.prefetch_attempts = vm->pipeline_metrics.prefetch_attempts,
.hot_word_count = hot_word_count,
.stale_word_count = stale_word_count,
.total_heat = total_heat,
.word_count = word_count,
.last_total_heat = vm->total_heat_at_last_check,
.last_stale_count = vm->stale_word_count_at_check
};
/* === Run Unified Inference Engine === */
inference_engine_run(&inference_inputs, vm->last_inference_outputs);
/* === Apply Inferred Tuning Parameters === */
if (!vm->last_inference_outputs->early_exited)
{
/* Full inference was executed (not cached by ANOVA early-exit) */
/* 1. Apply adaptive window width */
if (vm->last_inference_outputs->adaptive_window_width > 0 &&
vm->last_inference_outputs->adaptive_window_width != vm->rolling_window.effective_window_size)
{
log_message(LOG_DEBUG,
"INFERENCE[window]: %u → %u (variance=%.6f Q48.16)",
vm->rolling_window.effective_window_size,
vm->last_inference_outputs->adaptive_window_width,
(double)vm->last_inference_outputs->window_variance_q48 / 65536.0);
vm->rolling_window.effective_window_size = vm->last_inference_outputs->adaptive_window_width;
}
/* 2. Apply adaptive decay slope */
sf_mutex_lock(&vm->tuning_lock);
if (vm->last_inference_outputs->adaptive_decay_slope > 0 &&
vm->last_inference_outputs->adaptive_decay_slope != vm->decay_slope_q48)
{
double old_slope_dbl = (double)vm->decay_slope_q48 / 65536.0;
double new_slope_dbl = (double)vm->last_inference_outputs->adaptive_decay_slope / 65536.0;
log_message(LOG_DEBUG,
"INFERENCE[slope]: %.3f → %.3f (fit_quality=%.6f Q48.16)",
old_slope_dbl,
new_slope_dbl,
(double)vm->last_inference_outputs->slope_fit_quality_q48 / 65536.0);
vm->decay_slope_q48 = vm->last_inference_outputs->adaptive_decay_slope;
}
sf_mutex_unlock(&vm->tuning_lock);
/* 3. Validate outputs */
if (!inference_outputs_validate(vm->last_inference_outputs))
{
/* 2026-09-22: downgraded from LOG_WARN, matching the kernel
* copy of this logic (src/starkernel/vm/vm_runtime.c) --
* fires routinely during normal operation and the runtime
* self-recovers every time (the whole point of "ignoring
* results"), so it does not warrant default-visible
* attention. Still available with --log-level=info/debug. */
log_message(LOG_INFO,
"INFERENCE: Output validation failed, ignoring results");
}
vm->heartbeat.last_inference_tick = vm->heartbeat.tick_count;
}
else
{
/* ANOVA early-exit: variance stable, using cached outputs */
vm->heartbeat.early_exit_count++;
log_message(LOG_DEBUG,
"INFERENCE: Early-exit (variance stable <5%%), using cached outputs");
}
/* Store baseline for next inference comparison */
sf_mutex_lock(&vm->tuning_lock);
vm->total_heat_at_last_check = total_heat;
vm->stale_word_count_at_check = stale_word_count;
vm->word_count_at_check = word_count;
sf_mutex_unlock(&vm->tuning_lock);
/* L8 FINAL INTEGRATION: Loop always-on */ /* === Loop #7: Adaptive Heartrate ===
* Adjust tick frequency based on system stability:
* - Variance stable (early_exited) → increase tick interval (less frequent)
* - Variance volatile (full inference) → decrease tick interval (more frequent)
*
* Bounds: [HEARTBEAT_TICK_NS / 4, HEARTBEAT_TICK_NS * 4]
*/
{
uint64_t current_tick_ns = vm->heartbeat.tick_target_ns;
uint64_t min_tick_ns = HEARTBEAT_TICK_NS / 4; /* 4x faster minimum */
uint64_t max_tick_ns = HEARTBEAT_TICK_NS * 4; /* 4x slower maximum */
if (vm->last_inference_outputs && vm->last_inference_outputs->early_exited)
{
/* System stable: slow down heartbeat by 25% */
uint64_t new_tick_ns = (current_tick_ns * 125) / 100;
if (new_tick_ns > max_tick_ns) new_tick_ns = max_tick_ns;
if (new_tick_ns != current_tick_ns)
{
vm->heartbeat.tick_target_ns = new_tick_ns;
if (vm->heartbeat.worker)
vm->heartbeat.worker->tick_ns = new_tick_ns;
log_message(LOG_DEBUG,
"HEARTBEAT[rate]: stable → slower tick %lu → %lu ns",
(unsigned long)current_tick_ns,
(unsigned long)new_tick_ns);
}
}
else
{
/* System volatile: speed up heartbeat by 25% */
uint64_t new_tick_ns = (current_tick_ns * 80) / 100;
if (new_tick_ns < min_tick_ns) new_tick_ns = min_tick_ns;
if (new_tick_ns != current_tick_ns)
{
vm->heartbeat.tick_target_ns = new_tick_ns;
if (vm->heartbeat.worker)
vm->heartbeat.worker->tick_ns = new_tick_ns;
log_message(LOG_DEBUG,
"HEARTBEAT[rate]: volatile → faster tick %lu → %lu ns",
(unsigned long)current_tick_ns,
(unsigned long)new_tick_ns);
}
}
}
/* End always-on loop */}
/* ====================== Thread Start/Stop Wrappers ======================= */
void vm_heartbeat_start_thread(VM *vm)
{
#if HEARTBEAT_HAS_THREADS
if (!vm)
return;
vm->heartbeat.worker = sf_calloc(1, sizeof(HeartbeatWorker));
if (vm->heartbeat.worker)
{
vm->heartbeat.worker->tick_ns = HEARTBEAT_TICK_NS;
if (pthread_create(&vm->heartbeat.worker->thread, NULL, heartbeat_thread_main, vm) != 0)
{
log_message(LOG_WARN, "heartbeat: pthread_create failed (%d), falling back to inline mode", errno);
sf_free(vm->heartbeat.worker);
vm->heartbeat.worker = NULL;
}
}
else
{
log_message(LOG_WARN, "heartbeat: worker allocation failed, using inline heartbeat");
}
#else
(void)vm;
#endif
}
void vm_heartbeat_stop_thread(VM *vm)
{
#if HEARTBEAT_HAS_THREADS
if (!vm || !vm->heartbeat.worker)
return;
vm->heartbeat.worker->stop_requested = 1;
pthread_join(vm->heartbeat.worker->thread, NULL);
sf_free(vm->heartbeat.worker);
vm->heartbeat.worker = NULL;
#else
(void)vm;
#endif
}
+155
View File
@@ -0,0 +1,155 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "../include/word_registry.h"
#include "../include/log.h"
#include <string.h>
/* Include all FORTH-79 word module headers + extensions */
#include "word_source/include/acl_words.h"
#include "word_source/include/stack_words.h"
#include "word_source/include/return_stack_words.h"
#include "word_source/include/memory_words.h"
#include "word_source/include/dictionary_words.h"
#include "word_source/include/arithmetic_words.h"
#include "word_source/include/double_words.h"
#include "word_source/include/mixed_arithmetic_words.h"
#include "word_source/include/logical_words.h"
#include "word_source/include/io_words.h"
#include "word_source/include/format_words.h"
#include "word_source/include/string_words.h"
#include "word_source/include/control_words.h"
#include "word_source/include/defining_words.h"
#include "word_source/include/dictionary_manipulation_words.h"
#include "word_source/include/vocabulary_words.h"
#include "word_source/include/block_words.h"
#include "word_source/include/editor_words.h"
#include "word_source/include/system_words.h"
#include "word_source/include/starforth_words.h"
#include "word_source/include/physics_benchmark_words.h"
#include "word_source/include/physics_pipelining_diagnostic_words.h"
#include "word_source/include/physics_freeze_words.h"
#include "word_source/include/dictionary_heat_diagnostic_words.h"
#include "word_source/include/log_words.h"
#include "word_source/include/q48_words.h"
#include "word_source/include/inference_words.h"
#include "word_source/include/defer_words.h"
#include "word_source/include/framebuffer_words.h"
#include "word_source/include/keyboard_words.h"
#include "word_source/include/ttf_words.h"
#include "word_source/include/scroll_words.h"
#ifdef __STARKERNEL__
#include "starkernel/doe_log.h"
#include "starkernel/repl.h"
#endif
/**
* @brief Registers a single FORTH word in the virtual machine
* @param vm Pointer to the virtual machine instance
* @param name Name of the FORTH word to register
* @param func Function pointer to the word's implementation
*/
void register_word(VM *vm, const char *name, word_func_t func) {
DictEntry *entry = vm_create_word(vm, name, strlen(name), func);
if (entry) {
log_message(LOG_DEBUG, "Registered word: %s", name);
} else {
log_message(LOG_ERROR, "Failed to register word: %s", name);
}
}
/**
* @brief Registers all standard FORTH-79 words in the virtual machine
* @param vm Pointer to the virtual machine instance
* @details Registers all 18 FORTH-79 word modules in dependency order,
* starting with fundamental stack operations and building up to
* more complex functionality like control flow and editing.
*/
void register_forth79_words(VM *vm) {
log_message(LOG_INFO, "Registering FORTH-79 Standard word set...");
/* Register all 18 FORTH-79 word modules IN DEPENDENCY ORDER */
register_stack_words(vm); /* Module 1: Stack Operations - FOUNDATION */
register_return_stack_words(vm); /* Module 2: Return Stack Operations */
register_memory_words(vm); /* Module 3: Memory Operations */
register_arithmetic_words(vm); /* Module 4: Arithmetic Operations */
register_logical_words(vm); /* Module 5: Logical & Comparison */
register_mixed_arithmetic_words(vm); /* Module 6: Mixed Arithmetic - needs arithmetic + stack */
register_double_words(vm); /* Module 7: Double Number Arithmetic */
register_format_words(vm); /* Module 8: Formatting & Conversion */
register_string_words(vm); /* Module 9: String & Text Processing */
register_io_words(vm); /* Module 10: I/O & Terminal */
register_block_words(vm); /* Module 11: Block & Mass Storage */
register_dictionary_words(vm); /* Module 12: Dictionary & Compilation */
register_dictionary_manipulation_words(vm); /* Module 13: Dictionary Manipulation */
register_vocabulary_words(vm); /* Module 14: Vocabulary System */
register_system_words(vm); /* Module 15: System & Environment */
register_editor_words(vm); /* Module 16: Line Editor */
register_defining_words(vm); /* Module 17: Defining Words */
register_control_words(vm); /* Module 18: Control Flow */
register_starforth_words(vm); /* Module 19: StarForth Implementation Extensions */
register_acl_words(vm); /* Module 20: Word-Level ACL System */
register_physics_benchmark_words(vm); /* Module 21: Physics Benchmark Words */
register_physics_pipelining_diagnostic_words(vm); /* Module 21: Pipelining Diagnostics */
register_physics_freeze_words(vm); /* Module 22: Phase 2 Freeze/Decay Control Words */
register_dictionary_heat_diagnostic_words(vm); /* Module 23: Dictionary Heat Optimization */
register_log_words(vm); /* Module 24: Log Level Control */
register_q48_words(vm); /* Module 25: Q48.16 Fixed-Point Math */
register_inference_words(vm); /* Module 26: SSM Inference + Jacquard */
register_defer_words(vm); /* Module 27: DEFER / IS late binding */
/* Module 28 (PLOT/FB-WIDTH/FB-HEIGHT, framebuffer_words.c) deliberately
* NOT registered here -- FABRIC-3.6.md task 1.8 (item 33): every VM got
* these unconditionally before this change (confirmed live, task 0.8 --
* Hermes and Artemis could both call FB-WIDTH despite never loading
* fabric.4th). Hestia is now the sole owner; see the name-gated call in
* capsule_birth_baby() (capsule_birth.c), the only place a VM's name is
* known at the point its dictionary is built. */
register_keyboard_words(vm); /* Module 29: Console fabric -- raw keyboard scancode diagnostic */
register_ttf_words(vm); /* Module 30: TrueType text entry point */
register_scroll_words(vm); /* Module 31: REPL scrollback (FABRIC-0.md 4.4q) */
#ifdef __STARKERNEL__
register_doe_log_words(vm); /* Module 32: DoE instrumentation runtime toggle (HB-ON/HB-OFF) */
sk_repl_register_words(vm); /* Module 33: BLK-ATTACH-ACK, Artemis's storage-attach reply target */
#endif
log_message(LOG_INFO, "FORTH-79 Standard word set registration complete");
}
+51
View File
@@ -0,0 +1,51 @@
# src/word_source/
FORTH-79 word implementations, one file per category, registered into the
dictionary via `include/word_registry.h`. Each `.c` file here has a
matching header in `src/word_source/include/` and (for most categories) a
matching test module in `src/test_runner/modules/`.
**FORTH-79 core**
- `arithmetic_words.c`, `mixed_arithmetic_words.c` — `+ - * / MOD ABS MIN
MAX` and mixed-precision arithmetic.
- `stack_words.c` — `DUP DROP SWAP ROT OVER NIP TUCK`.
- `control_words.c` — `IF ELSE THEN DO LOOP BEGIN UNTIL WHILE`.
- `defining_words.c` — `: ; CREATE DOES> VARIABLE CONSTANT`.
- `memory_words.c` — `@ ! C@ C! MOVE FILL`.
- `return_stack_words.c` — `>R R> R@ RDROP 2>R 2R@ 2R>`.
- `double_words.c` — `2DUP 2DROP 2SWAP 2@ 2! D+ D-`.
- `logical_words.c` — `AND OR XOR NOT INVERT LSHIFT RSHIFT`.
- `io_words.c` — `EMIT KEY TYPE CR TAB SPACE ACCEPT`.
- `string_words.c` — `S" SLITERAL` and other string operations.
- `block_words.c` — `BLOCK BUFFER LOAD THRU FLUSH`.
- `format_words.c` — `.( .R .S HEX DECIMAL BASE`.
- `system_words.c` — `BYE ABORT INCLUDE STATE`.
- `dictionary_words.c` — `FIND SEARCH-WORDLIST WORDS`.
- `dictionary_manipulation_words.c` — dictionary entry manipulation words.
- `vocabulary_words.c` — `VOCABULARY DEFINITIONS FORTH-WORDLIST`.
- `editor_words.c` — block editor words.
- `defer_words.c` — `DEFER`/`IS` deferred-word mechanism, used by the
Tripod message-dispatch shim (see `tools/hermes_tripod_smoke.sh`).
**StarForth-specific extensions**
- `q48_16_words.c`, `q48_words.c` — Q48.16 fixed-point word bindings.
- `starforth_words.c` — StarForth-specific extensions.
- `acl_words.c` — word-level ACL system's FORTH-callable primitives (see
`docs/03-architecture/word-acl/`).
- `lifecycle_words_hosted.c` — hosted-build VM lifecycle words (birth/run
primitives on the hosted side; kernel-only primitives live in
`src/starkernel/capsule/mama_forth_words.c`).
- `log_words.c` — FORTH-callable logging primitives.
- `inference_words.c` — FORTH-callable bindings for the statistical
inference engine (Loops #5/#6).
- `physics_benchmark_words.c` — benchmark harness for the 7 physics
feedback loops.
- `physics_diagnostic_words.c` — physics diagnostics (`WORD-ENTROPY`).
- `physics_freeze_words.c` — `PHYSICS-FREEZE` / `PHYSICS-THAW`.
- `physics_pipelining_diagnostic_words.c` — Loop #4 pipelining
diagnostics.
- `dictionary_heat_diagnostic_words.c` — Loop #1 heat diagnostics.
See `src/word_source/include/` for the matching headers (also includes
`mama_forth_words.h`, whose `.c` implementation lives under
`src/starkernel/capsule/` since it's kernel-only).
+355
View File
@@ -0,0 +1,355 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* acl_words.c — C primitive words for the word-level ACL system
*
* All policy logic lives in capsules/ACL.4th. These primitives expose
* DictEntry ACL fields to FORTH: ACL-MODE@, ACL-MODE!, ACL-PINNED?,
* ACL-PIN, ACL-TTL@, ACL-TTL!, ACL-ALLOW@, ACL-ALLOW!, ACL-HEAT@,
* ACL-INHERIT, ACL-INIT-PRIMITIVES, ACL-WORD-ID.
*
* All words that take an XT expect a DictEntry* cast to cell_t on the
* stack — exactly what ' (tick) produces in StarForth.
*/
#include "include/acl_words.h"
#include "../../include/vm.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
/**
* @brief Pop an execution token (XT) from the data stack and return the
* corresponding DictEntry pointer.
*
* Sets @c vm->error and returns NULL on stack underflow or if the XT is zero.
* All ACL word implementations call this before accessing DictEntry fields.
*
* @param vm Pointer to the VM structure
* @return Pointer to the DictEntry, or NULL on error
*/
static DictEntry *pop_xt(VM *vm)
{
if (!vm || vm->dsp < 0) {
log_message(LOG_ERROR, "ACL word: stack underflow");
if (vm) vm->error = 1;
return NULL;
}
cell_t xt = vm_pop(vm);
DictEntry *entry = (DictEntry *)(uintptr_t)xt;
if (!entry) {
log_message(LOG_ERROR, "ACL word: NULL XT");
vm->error = 1;
return NULL;
}
return entry;
}
/**
* @brief Implements FORTH word @c ACL-MODE@ — fetch ACL enforcement mode
*
* Stack effect: ( xt -- mode )
* Pushes the enforcement mode of the word identified by @c xt:
* 0 = STRICT (allow/deny is permanent), 1 = TTL (expires after countdown).
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_mode_fetch(VM *vm)
{
DictEntry *e = pop_xt(vm);
if (!e) return;
vm_push(vm, (cell_t)e->acl_mode);
}
/**
* @brief Implements FORTH word @c ACL-MODE! — store ACL enforcement mode
*
* Stack effect: ( mode xt -- )
* Sets the enforcement mode of the word identified by @c xt.
* No-op if the word is pinned — pinned entries are immutable.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_mode_store(VM *vm)
{
if (!vm || vm->dsp < 1) {
log_message(LOG_ERROR, "ACL-MODE!: stack underflow");
if (vm) vm->error = 1;
return;
}
DictEntry *e = pop_xt(vm);
if (!e) return;
cell_t mode = vm_pop(vm);
if (e->acl_pinned) return; /* one-way ratchet: pinned is immutable */
e->acl_mode = (uint8_t)(mode & 0xFF);
}
/**
* @brief Implements FORTH word @c ACL-PINNED? — query pin status
*
* Stack effect: ( xt -- flag )
* Pushes -1 (true) if the word is pinned, 0 (false) otherwise.
* A pinned word's ACL fields cannot be modified by any FORTH operation.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_pinned_query(VM *vm)
{
DictEntry *e = pop_xt(vm);
if (!e) return;
vm_push(vm, e->acl_pinned ? -1 : 0);
}
/**
* @brief Implements FORTH word @c ACL-PIN — pin a word's ACL entry
*
* Stack effect: ( xt -- )
* Sets @c acl_pinned = 1 on the word identified by @c xt.
* This is a one-way ratchet: once set it can never be cleared, even by
* ACL-INHERIT. Use with @c ' WORD ACL-PIN in FORTH, never from C policy code.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_pin(VM *vm)
{
DictEntry *e = pop_xt(vm);
if (!e) return;
e->acl_pinned = 1;
}
/**
* @brief Implements FORTH word @c ACL-TTL@ — fetch TTL countdown value
*
* Stack effect: ( xt -- n )
* Pushes the current TTL (time-to-live) countdown of the word's ACL entry.
* When TTL reaches zero in TTL mode the interpreter calls @c acl_recheck().
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_ttl_fetch(VM *vm)
{
DictEntry *e = pop_xt(vm);
if (!e) return;
vm_push(vm, (cell_t)e->acl_ttl);
}
/**
* @brief Implements FORTH word @c ACL-TTL! — store TTL countdown value
*
* Stack effect: ( n xt -- )
* Sets the TTL countdown on the word's ACL entry. Negative values are
* clamped to 0; values above UINT32_MAX are clamped to UINT32_MAX.
* No-op if the word is pinned.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_ttl_store(VM *vm)
{
if (!vm || vm->dsp < 1) {
log_message(LOG_ERROR, "ACL-TTL!: stack underflow");
if (vm) vm->error = 1;
return;
}
DictEntry *e = pop_xt(vm);
if (!e) return;
cell_t n = vm_pop(vm);
if (e->acl_pinned) return;
e->acl_ttl = (uint32_t)(n < 0 ? 0 : (uint64_t)n > UINT32_MAX ? UINT32_MAX : (uint32_t)n);
}
/**
* @brief Implements FORTH word @c ACL-ALLOW@ — fetch cached allow/deny flag
*
* Stack effect: ( xt -- flag )
* Pushes the cached ACL decision for the word: -1 = allowed, 0 = denied.
* This cached value is checked on every execution in TTL mode; the cache is
* refreshed by @c acl_recheck() when TTL expires.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_allow_fetch(VM *vm)
{
DictEntry *e = pop_xt(vm);
if (!e) return;
vm_push(vm, e->acl_allow ? -1 : 0);
}
/**
* @brief Implements FORTH word @c ACL-ALLOW! — store cached allow/deny flag
*
* Stack effect: ( flag xt -- )
* Sets the cached ACL decision: any non-zero @c flag = allowed, 0 = denied.
* No-op if the word is pinned.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_allow_store(VM *vm)
{
if (!vm || vm->dsp < 1) {
log_message(LOG_ERROR, "ACL-ALLOW!: stack underflow");
if (vm) vm->error = 1;
return;
}
DictEntry *e = pop_xt(vm);
if (!e) return;
cell_t flag = vm_pop(vm);
if (e->acl_pinned) return;
e->acl_allow = (flag != 0) ? 1 : 0;
}
/**
* @brief Implements FORTH word @c ACL-HEAT@ — fetch execution heat
*
* Stack effect: ( xt -- heat )
* Pushes the @c execution_heat counter for the word identified by @c xt.
* Used by @c ACL-TTL-COMPUTE in @c ACL.4th to calibrate the TTL value based
* on how frequently the word is executed.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_heat_fetch(VM *vm)
{
DictEntry *e = pop_xt(vm);
if (!e) return;
vm_push(vm, (cell_t)e->execution_heat);
}
/**
* @brief Implements FORTH word @c ACL-WORD-ID — fetch stable word identifier
*
* Stack effect: ( xt -- n )
* Pushes the stable @c word_id for the entry identified by @c xt. Unlike
* dictionary positions, @c word_id is assigned at registration and never
* changes, making it safe to use as a persistent table index in @c ACL.4th.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_word_id(VM *vm)
{
DictEntry *e = pop_xt(vm);
if (!e) return;
vm_push(vm, (cell_t)e->word_id);
}
/**
* @brief Implements FORTH word @c ACL-INHERIT — inherit ACL policy from parent
*
* Stack effect: ( src dst -- )
* Copies @c acl_mode from @c src to @c dst, then clears @c dst->acl_pinned,
* resets @c acl_ttl to 0, and sets @c acl_allow to 1 (optimistic default).
* This is a C primitive because FORTH code cannot clear @c acl_pinned — pin
* is a one-way ratchet that only C can reset during inheritance.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_inherit(VM *vm)
{
if (!vm || vm->dsp < 1) {
log_message(LOG_ERROR, "ACL-INHERIT: stack underflow");
if (vm) vm->error = 1;
return;
}
DictEntry *dst = pop_xt(vm);
if (!dst) return;
DictEntry *src = pop_xt(vm);
if (!src) return;
acl_inherit_entry(src, dst);
}
/**
* @brief Implements FORTH word @c ACL-INIT-PRIMITIVES — reset ACL fields on all
* unpinned dictionary entries
*
* Stack effect: ( -- )
* Walks the entire dictionary and resets ACL fields on every entry that is not
* pinned: sets @c acl_ttl = 0 (force recheck on first execution), @c acl_allow
* = 1 (optimistic default), @c acl_mode = ACL_MODE_TTL. Called by @c ACL-BOOT
* in @c ACL.4th immediately after the ACL capsule finishes loading.
*
* @param vm Pointer to the VM structure
*/
static void forth_acl_init_primitives(VM *vm)
{
if (!vm) return;
for (DictEntry *e = vm->latest; e; e = e->link) {
if (!e->acl_pinned) {
/* Re-assert permissive defaults; ACL-RECHECK will calibrate on first exec */
e->acl_ttl = 0; /* force recheck on first execution */
e->acl_allow = 1;
e->acl_mode = ACL_MODE_TTL;
}
}
}
/**
* @brief Direct C entry point for ACL policy inheritance on the capsule birth path.
*
* Copies @c acl_mode from @c src to @c dst, clears @c acl_pinned, resets
* @c acl_ttl to 0, and sets @c acl_allow to 1. Called from capsule birth code
* (not from FORTH) — no stack manipulation is performed.
*
* @param src Source DictEntry whose @c acl_mode is inherited
* @param dst Destination DictEntry whose ACL fields are updated
*/
void acl_inherit_entry(DictEntry *src, DictEntry *dst)
{
if (!src || !dst) return;
dst->acl_mode = src->acl_mode;
dst->acl_pinned = 0; /* pin is contextual, not viral */
dst->acl_ttl = 0; /* child has no history; force recheck */
dst->acl_allow = 1; /* optimistic default */
}
/**
* @brief Register all ACL C-primitive words with the VM dictionary.
*
* Registers: field accessors (@c ACL-MODE@ / @c ACL-MODE! / @c ACL-PINNED? /
* @c ACL-TTL@ / @c ACL-TTL! / @c ACL-ALLOW@ / @c ACL-ALLOW! / @c ACL-HEAT@ /
* @c ACL-WORD-ID), and mutation words (@c ACL-PIN / @c ACL-INHERIT /
* @c ACL-INIT-PRIMITIVES). Called from @c vm_bootstrap.c during VM
* initialisation.
*
* @param vm Pointer to the VM structure
*/
void register_acl_words(VM *vm)
{
/* Field accessors — read */
register_word(vm, "ACL-MODE@", forth_acl_mode_fetch);
register_word(vm, "ACL-PINNED?", forth_acl_pinned_query);
register_word(vm, "ACL-TTL@", forth_acl_ttl_fetch);
register_word(vm, "ACL-ALLOW@", forth_acl_allow_fetch);
register_word(vm, "ACL-HEAT@", forth_acl_heat_fetch);
register_word(vm, "ACL-WORD-ID", forth_acl_word_id);
/* Field accessors — write (all no-op if pinned) */
register_word(vm, "ACL-MODE!", forth_acl_mode_store);
register_word(vm, "ACL-TTL!", forth_acl_ttl_store);
register_word(vm, "ACL-ALLOW!", forth_acl_allow_store);
/* Pin ratchet — one-way set */
register_word(vm, "ACL-PIN", forth_acl_pin);
/* Higher-level operations */
register_word(vm, "ACL-INHERIT", forth_acl_inherit);
register_word(vm, "ACL-INIT-PRIMITIVES", forth_acl_init_primitives);
}
+438
View File
@@ -0,0 +1,438 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* arithmetic_words.c - FORTH-79 Arithmetic Words */
#include "include/arithmetic_words.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
/* FORTH-79 Arithmetic Words:
* + ( n1 n2 -- n3 ) Add n1 and n2
* - ( n1 n2 -- n3 ) Subtract n2 from n1
* * ( n1 n2 -- n3 ) Multiply n1 and n2
* / ( n1 n2 -- n3 ) Divide n1 by n2
* MOD ( n1 n2 -- n3 ) n1 modulo n2
* /MOD ( n1 n2 -- n3 n4 ) n1/n2 remainder and quotient
* STAR-SLASH ( n1 n2 n3 -- n4 ) n1*n2/n3 with intermediate double
* STAR-SLASH-MOD ( n1 n2 n3 -- n4 n5 ) n1*n2/n3 remainder and quotient
* 1+ ( n -- n+1 ) Add 1
* 1- ( n -- n-1 ) Subtract 1
* 2+ ( n -- n+2 ) Add 2
* 2- ( n -- n-2 ) Subtract 2
* 2* ( n -- n*2 ) Multiply by 2 (left shift)
* 2/ ( n -- n/2 ) Divide by 2 (right shift)
* ABS ( n -- |n| ) Absolute value
* NEGATE ( n -- -n ) Two's complement
* MIN ( n1 n2 -- n3 ) Minimum of n1 and n2
* MAX ( n1 n2 -- n3 ) Maximum of n1 and n2
*/
/* Forward declarations */
static void arithmetic_word_star_slash(VM * vm);
static void arithmetic_word_star_slash_mod(VM * vm);
/**
* @brief Implements FORTH word "+" that adds two numbers
*
* Stack effect: ( n1 n2 -- n3 )
* Adds n1 and n2, pushing their sum n3 onto the stack.
*
* @param vm Pointer to the VM structure
*/
void arithmetic_word_plus(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "+: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
cell_t result = n1 + n2;
vm_push(vm, result);
log_message(LOG_DEBUG, "+: %ld + %ld = %ld", n1, n2, result);
}
/**
* @brief Implements FORTH word "-" that subtracts two numbers
*
* Stack effect: ( n1 n2 -- n3 )
* Subtracts n2 from n1, pushing their difference n3 onto the stack.
*
* @param vm Pointer to the VM structure
*/
static void arithmetic_word_minus(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "-: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
cell_t result = n1 - n2;
vm_push(vm, result);
log_message(LOG_DEBUG, "-: %ld - %ld = %ld", n1, n2, result);
}
/**
* @brief Implements FORTH word "*" that multiplies two numbers
*
* Stack effect: ( n1 n2 -- n3 )
* Multiplies n1 by n2, pushing their product n3 onto the stack.
*
* @param vm Pointer to the VM structure
*/
static void arithmetic_word_multiply(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "*: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
cell_t result = n1 * n2;
vm_push(vm, result);
log_message(LOG_DEBUG, "*: %ld * %ld = %ld", n1, n2, result);
}
/* / ( n1 n2 -- n3 ) Divide n1 by n2 */
static void word_divide(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "/: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
if (n2 == 0) {
log_message(LOG_ERROR, "/: Division by zero");
vm->error = 1;
return;
}
cell_t result = n1 / n2;
vm_push(vm, result);
log_message(LOG_DEBUG, "/: %ld / %ld = %ld", n1, n2, result);
}
/* MOD ( n1 n2 -- n3 ) n1 modulo n2 */
static void mixed_math_word_mod(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "MOD: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
if (n2 == 0) {
log_message(LOG_ERROR, "MOD: Division by zero");
vm->error = 1;
return;
}
cell_t result = n1 % n2;
vm_push(vm, result);
log_message(LOG_DEBUG, "MOD: %ld MOD %ld = %ld", n1, n2, result);
}
/* /MOD ( n1 n2 -- n3 n4 ) n1/n2 remainder and quotient */
static void word_divmod(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "/MOD: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
if (n2 == 0) {
log_message(LOG_ERROR, "/MOD: Division by zero");
vm->error = 1;
return;
}
cell_t remainder = n1 % n2;
cell_t quotient = n1 / n2;
vm_push(vm, remainder); /* remainder first */
vm_push(vm, quotient); /* quotient on top */
log_message(LOG_DEBUG, "/MOD: %ld /MOD %ld = %ld remainder %ld", n1, n2, quotient, remainder);
}
/* STAR-SLASH ( n1 n2 n3 -- n4 ) n1*n2/n3 with intermediate double */
static void arithmetic_word_star_slash(VM *vm) {
if (vm->dsp < 2) {
log_message(LOG_ERROR, "*/: Stack underflow");
vm->error = 1;
return;
}
cell_t n3 = vm_pop(vm); /* divisor */
cell_t n2 = vm_pop(vm); /* multiplier */
cell_t n1 = vm_pop(vm); /* multiplicand */
if (n3 == 0) {
log_message(LOG_ERROR, "*/: Division by zero");
vm->error = 1;
return;
}
/* Use 64-bit intermediate to avoid overflow */
int64_t intermediate = n1 * n2;
cell_t result = (intermediate / n3);
vm_push(vm, result);
log_message(LOG_DEBUG, "*/: %ld * %ld / %ld = %ld", n1, n2, n3, result);
}
/* STAR-SLASH-MOD ( n1 n2 n3 -- n4 n5 ) n1*n2/n3 remainder and quotient */
static void arithmetic_word_star_slash_mod(VM *vm) {
if (vm->dsp < 2) {
log_message(LOG_ERROR, "*/MOD: Stack underflow");
vm->error = 1;
return;
}
cell_t n3 = vm_pop(vm); /* divisor */
cell_t n2 = vm_pop(vm); /* multiplier */
cell_t n1 = vm_pop(vm); /* multiplicand */
if (n3 == 0) {
log_message(LOG_ERROR, "*/MOD: Division by zero");
vm->error = 1;
return;
}
/* Use 64-bit intermediate to avoid overflow */
int64_t intermediate = n1 * n2;
cell_t remainder = (intermediate % n3);
cell_t quotient = (intermediate / n3);
vm_push(vm, remainder); /* remainder first */
vm_push(vm, quotient); /* quotient on top */
}
/* 1+ ( n -- n+1 ) Add 1 */
static void arithmetic_word_one_plus(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "1+: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_push(vm, n + 1);
log_message(LOG_DEBUG, "1+: %ld + 1 = %ld", n, (n + 1));
}
/* 1- ( n -- n-1 ) Subtract 1 */
static void arithmetic_word_one_minus(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "1-: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_push(vm, n - 1);
log_message(LOG_DEBUG, "1-: %ld - 1 = %ld", n, (n - 1));
}
/* 2+ ( n -- n+2 ) Add 2 */
static void arithmetic_word_two_plus(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "2+: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_push(vm, n + 2);
log_message(LOG_DEBUG, "2+: %ld + 2 = %ld", n, (n + 2));
}
/* 2- ( n -- n-2 ) Subtract 2 */
static void arithmetic_word_two_minus(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "2-: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_push(vm, n - 2);
log_message(LOG_DEBUG, "2-: %ld - 2 = %ld", n, (n - 2));
}
/* 2* ( n -- n*2 ) Multiply by 2 (left shift) */
static void arithmetic_word_two_multiply(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "2*: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_push(vm, n << 1);
log_message(LOG_DEBUG, "2*: %ld * 2 = %ld", n, (n << 1));
}
/* 2/ ( n -- n/2 ) Divide by 2 (right shift) */
static void arithmetic_word_two_divide(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "2/: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_push(vm, n >> 1);
log_message(LOG_DEBUG, "2/: %ld / 2 = %ld", n, (n >> 1));
}
/* ABS ( n -- |n| ) Absolute value */
static void arithmetic_word_abs(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "ABS: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
cell_t result = (n < 0) ? -n : n;
vm_push(vm, result);
log_message(LOG_DEBUG, "ABS: |%ld| = %ld", n, result);
}
/* NEGATE ( n -- -n ) Two's complement */
static void arithmetic_word_negate(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "NEGATE: Stack underflow");
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_push(vm, -n);
log_message(LOG_DEBUG, "NEGATE: -%ld = %ld", n, (-n));
}
/* MIN ( n1 n2 -- n3 ) Minimum of n1 and n2 */
static void arithmetic_word_min(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "MIN: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
cell_t result = (n1 < n2) ? n1 : n2;
vm_push(vm, result);
log_message(LOG_DEBUG, "MIN: min(%ld, %ld) = %ld", n1, n2, result);
}
/* MAX ( n1 n2 -- n3 ) Maximum of n1 and n2 */
static void arithmetic_word_max(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "MAX: Stack underflow");
vm->error = 1;
return;
}
cell_t n2 = vm_pop(vm);
cell_t n1 = vm_pop(vm);
cell_t result = (n1 > n2) ? n1 : n2;
vm_push(vm, result);
log_message(LOG_DEBUG, "MAX: max(%ld, %ld) = %ld", n1, n2, result);
}
/*
* @brief Registers all FORTH-79 arithmetic words with the VM
*
* This function registers all the standard FORTH-79 arithmetic operations
* including basic arithmetic, advanced math operations, increment/decrement
* operations, and comparison functions.
*
* @param vm Pointer to the VM structure where words will be registered
*/
void register_arithmetic_words(VM *vm) {
log_message(LOG_INFO, "Registering FORTH-79 arithmetic words...");
/* Basic arithmetic */
register_word(vm, "+", arithmetic_word_plus);
register_word(vm, "-", arithmetic_word_minus);
register_word(vm, "*", arithmetic_word_multiply);
register_word(vm, "/", word_divide);
register_word(vm, "MOD", mixed_math_word_mod);
register_word(vm, "/MOD", word_divmod);
/* Advanced arithmetic */
register_word(vm, "*/", arithmetic_word_star_slash);
register_word(vm, "*/MOD", arithmetic_word_star_slash_mod);
/* Increment/decrement */
register_word(vm, "1+", arithmetic_word_one_plus);
register_word(vm, "1-", arithmetic_word_one_minus);
register_word(vm, "2+", arithmetic_word_two_plus);
register_word(vm, "2-", arithmetic_word_two_minus);
register_word(vm, "2*", arithmetic_word_two_multiply);
register_word(vm, "2/", arithmetic_word_two_divide);
/* Sign and comparison */
register_word(vm, "ABS", arithmetic_word_abs);
register_word(vm, "NEGATE", arithmetic_word_negate);
register_word(vm, "MIN", arithmetic_word_min);
register_word(vm, "MAX", arithmetic_word_max);
}
+703
View File
@@ -0,0 +1,703 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
*** StarForth ***
block_words.c - FORTH-79 Block Words (Layer 3: Forth Interface)
Last modified - 10/02/25, 03:55 PM ET
Author: Robert A. James (rajames) - StarshipOS Forth Project.
License: Creative Commons Zero v1.0 Universal
<http://creativecommons.org/publicdomain/zero/1.0/>
*/
#include "include/block_words.h"
#include "../../include/word_registry.h"
#include "../../include/vm.h"
#include "../../include/block_subsystem.h"
#include <string.h>
#include <stdio.h>
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium_blocks.h" /* stadium_block_dispatch() -- FABRIC-2.md §B */
#endif
/* ----------------------------------------------------------------------
* Architecture:
* - Layer 1: blkio (vtable abstraction)
* - Layer 2: block_subsystem (RAM 0-1023, disk 1024+, 4KB packing)
* - Layer 3: block_words (THIS FILE - Forth interface)
*
* All block I/O now goes through block_subsystem.h API.
* Block 0 is RESERVED for volume metadata.
* ---------------------------------------------------------------------- */
static void set_scr(VM *vm, cell_t blk) {
if (!vm) return;
vm_store_cell(vm, vm->scr_addr, (cell_t) blk);
}
/* Optional utility surface (matches your header) */
/*
* @brief Initializes the block system
* @param vm Pointer to the VM instance
*/
void init_block_system(VM *vm) {
(void) vm;
/* Subsystem initialization happens in main.c via blk_subsys_init() */
}
/**
* @brief Gets a buffer for the specified block
* @param vm Pointer to the VM instance
* @param block_num Block number to retrieve
* @return Pointer to block buffer or NULL if invalid
*/
unsigned char *get_block_buffer(VM *vm, int block_num) {
if (!vm) return NULL;
if (block_num == 0) return NULL; /* Block 0 reserved */
uint8_t *buf = blk_get_buffer((uint32_t) block_num, 0); /* read-only */
if (buf) {
set_scr(vm, (cell_t) block_num);
}
return buf;
}
/**
* @brief Gets an empty buffer for the specified block
* @param vm Pointer to the VM instance
* @param block_num Block number to create
* @return Pointer to empty block buffer or NULL if invalid
*/
unsigned char *get_empty_buffer(VM *vm, int block_num) {
if (!vm) return NULL;
if (block_num == 0) return NULL; /* Block 0 reserved */
uint8_t *buf = blk_get_empty_buffer((uint32_t) block_num);
if (buf) {
set_scr(vm, (cell_t) block_num);
}
return buf;
}
/*
* @brief Marks the current block buffer as dirty
* @param vm Pointer to the VM instance
*/
void mark_buffer_dirty(VM *vm) {
if (!vm) return;
cell_t blk = vm_load_cell(vm, vm->scr_addr);
if (blk > 0) {
blk_update((uint32_t) blk);
}
}
/*
* @brief Saves all dirty buffers
* @param vm Pointer to the VM instance
*/
void save_all_buffers(VM *vm) {
(void) vm;
blk_flush(0); /* flush all */
}
/**
* @brief Empties all user block buffers
* @param vm Pointer to the VM instance
*/
void empty_all_buffers(VM *vm) {
if (!vm) return;
/* Zero blocks USER_BLOCKS_START through max available */
uint32_t total = blk_get_total_blocks();
for (uint32_t blk = USER_BLOCKS_START; blk < total; ++blk) {
uint8_t *buf = blk_get_buffer(blk, 1); /* writable */
if (buf) {
memset(buf, 0, BLOCK_SIZE);
}
}
}
/* --- Block I/O window helpers ----------------------------------------- */
/* Discard the whole VM block window if the device chain has changed since
* it was last validated (Milestone 2h). A device hot-detach followed by a
* later re-attach can reuse both the exact same LBN range
* (block_subsystem.c's chain always appends at the current tail) *and* the
* exact same blk_get_buffer() return address (confirmed live: this
* kernel's own first-fit kmalloc, src/starkernel/memory/kmalloc.c, hands
* the just-freed slot straight back to the very next same-size
* calloc()), so neither LBN nor a cached pointer is a reliable
* "still the same device" signal on its own. Any epoch change discards
* every cached slot outright -- no flush attempt, matching
* blk_subsys_detach_device()'s own reasoning: whatever device the stale
* content belonged to may already be gone by the time this runs. Called at
* 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. */
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++) {
vm->blk_vm_lbn[i] = 0;
vm->blk_vm_cbuf[i] = NULL;
vm->blk_vm_dirty[i] = 0;
}
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);
for (int i = 0; i < BLK_VM_SLOTS; i++) {
if (vm->blk_vm_lbn[i] == lbn && vm->blk_vm_cbuf[i] != NULL)
return i;
}
return -1;
}
/* Evict one slot (write back if dirty), return the freed slot index. */
static int blk_vm_evict(VM *vm) {
/* Prefer a clean slot to avoid unnecessary writeback. */
for (int i = 0; i < BLK_VM_SLOTS; i++) {
int s = (vm->blk_vm_next + i) % BLK_VM_SLOTS;
if (vm->blk_vm_cbuf[s] == NULL || !vm->blk_vm_dirty[s]) {
vm->blk_vm_lbn[s] = 0;
vm->blk_vm_cbuf[s] = NULL;
vm->blk_vm_dirty[s] = 0;
vm->blk_vm_next = (s + 1) % BLK_VM_SLOTS;
return s;
}
}
/* All slots dirty: evict round-robin, sync back to C buffer first.
* Re-resolve the buffer pointer by LBN rather than trusting the
* stored one -- the block-subsystem's own devblock cache may have
* shifted (struct-copy eviction) since this slot was populated,
* which silently invalidates any raw pointer captured earlier. */
int s = vm->blk_vm_next;
vm->blk_vm_next = (vm->blk_vm_next + 1) % BLK_VM_SLOTS;
vaddr_t base = BLK_VM_WINDOW_BASE + (vaddr_t)s * BLOCK_SIZE;
uint8_t* fresh = blk_get_buffer(vm->blk_vm_lbn[s], 1);
if (fresh)
{
memcpy(fresh, vm->memory + base, BLOCK_SIZE);
blk_update(vm->blk_vm_lbn[s]);
}
vm->blk_vm_lbn[s] = 0;
vm->blk_vm_cbuf[s] = NULL;
vm->blk_vm_dirty[s] = 0;
return s;
}
/* Load lbn into a window slot (reads C buffer), return VM offset or 0 on err. */
static vaddr_t blk_vm_load(VM *vm, uint32_t lbn, int writable) {
int s = blk_vm_find(vm, lbn);
if (s < 0) {
s = -1;
for (int i = 0; i < BLK_VM_SLOTS; i++) {
if (vm->blk_vm_cbuf[i] == NULL) { s = i; break; }
}
if (s < 0) s = blk_vm_evict(vm);
uint8_t *cbuf = blk_get_buffer(lbn, writable);
if (!cbuf) return 0;
vaddr_t base = BLK_VM_WINDOW_BASE + (vaddr_t)s * BLOCK_SIZE;
memcpy(vm->memory + base, cbuf, BLOCK_SIZE);
vm->blk_vm_lbn[s] = lbn;
vm->blk_vm_cbuf[s] = cbuf;
vm->blk_vm_dirty[s] = 0;
}
return BLK_VM_WINDOW_BASE + (vaddr_t)s * BLOCK_SIZE;
}
/* Assign lbn to a slot WITHOUT reading block content (BUFFER semantics).
* Returns VM offset or 0 on error. Slot is zeroed and marked dirty. */
static vaddr_t blk_vm_assign(VM *vm, uint32_t lbn) {
int s = blk_vm_find(vm, lbn);
if (s < 0) {
s = -1;
for (int i = 0; i < BLK_VM_SLOTS; i++) {
if (vm->blk_vm_cbuf[i] == NULL) { s = i; break; }
}
if (s < 0) s = blk_vm_evict(vm);
uint8_t *cbuf = blk_get_empty_buffer(lbn);
if (!cbuf) return 0;
vaddr_t base = BLK_VM_WINDOW_BASE + (vaddr_t)s * BLOCK_SIZE;
memset(vm->memory + base, 0, BLOCK_SIZE);
vm->blk_vm_lbn[s] = lbn;
vm->blk_vm_cbuf[s] = cbuf;
}
vm->blk_vm_dirty[s] = 1;
return BLK_VM_WINDOW_BASE + (vaddr_t)s * BLOCK_SIZE;
}
/* Sync all dirty slots to their C buffers and flush the subsystem.
* Re-resolve each buffer pointer by LBN rather than trusting the stored
* one -- see blk_vm_evict for why a stored pointer can go stale. Checks
* the epoch first (Milestone 2h, see blk_vm_check_epoch()) since this
* function walks vm->blk_vm_lbn[]/vm->blk_vm_cbuf[] directly rather than
* through blk_vm_find() -- a stale-epoch dirty slot must be discarded, not
* flushed onto whatever device now owns that LBN.
*
* Non-static: this is the entire implementation behind SAVE-BUFFERS
* (block_word_save_buffers() below is a one-line wrapper) -- exposed
* (declared in block_words.h) so kernel-side code can reuse the exact
* same flush path outside the word-dispatch mechanism, e.g. sk_repl_idle()
* (see FABRIC-0.md/FABRIC-1.md Section V item 6). Cheap to call when
* nothing is dirty: every check below is a small fixed-size scan
* (BLK_VM_SLOTS here, DISK_CACHE_SLOTS per device inside blk_flush()),
* no I/O happens unless something actually needs writing. */
void blk_vm_flush_all(VM *vm) {
blk_vm_check_epoch(vm);
for (int i = 0; i < BLK_VM_SLOTS; i++) {
if (vm->blk_vm_cbuf[i] != NULL && vm->blk_vm_dirty[i]) {
vaddr_t base = BLK_VM_WINDOW_BASE + (vaddr_t)i * BLOCK_SIZE;
uint8_t* fresh = blk_get_buffer(vm->blk_vm_lbn[i], 1);
if (fresh)
{
memcpy(fresh, vm->memory + base, BLOCK_SIZE);
blk_update(vm->blk_vm_lbn[i]);
}
vm->blk_vm_dirty[i] = 0;
}
}
blk_flush(0);
}
/* --- Words ------------------------------------------------------------ */
/* BLOCK ( u -- addr ) : VM address of block u content (no dirty mark) */
void block_word_block(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
#ifdef __STARKERNEL__
stadium_block_dispatch(vm->stadium_vm_id, (uint32_t) blk, vm->heartbeat.tick_count);
#endif
vaddr_t vaddr = blk_vm_load(vm, (uint32_t) blk, 0);
if (!vaddr) { vm->error = 1; return; }
set_scr(vm, blk);
vm_push(vm, CELL(vaddr));
}
/* BUFFER ( u -- addr ) : VM address of block u; content unread, mark dirty */
void block_word_buffer(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
#ifdef __STARKERNEL__
stadium_block_dispatch(vm->stadium_vm_id, (uint32_t) blk, vm->heartbeat.tick_count);
#endif
vaddr_t vaddr = blk_vm_assign(vm, (uint32_t) blk);
if (!vaddr) { vm->error = 1; return; }
set_scr(vm, blk);
vm_push(vm, CELL(vaddr));
}
/* UPDATE ( -- ) : sync current SCR slot to C layer and mark dirty.
* Re-resolve the buffer pointer by LBN rather than trusting the stored
* one -- see blk_vm_evict for why a stored pointer can go stale. */
void block_word_update(VM *vm) {
cell_t blk = vm_load_cell(vm, vm->scr_addr);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
#ifdef __STARKERNEL__
stadium_block_dispatch(vm->stadium_vm_id, (uint32_t) blk, vm->heartbeat.tick_count);
#endif
int s = blk_vm_find(vm, (uint32_t) blk);
if (s < 0) { vm->error = 1; return; }
vaddr_t base = BLK_VM_WINDOW_BASE + (vaddr_t)s * BLOCK_SIZE;
uint8_t* fresh = blk_get_buffer((uint32_t)blk, 1);
if (!fresh)
{
vm->error = 1;
return;
}
memcpy(fresh, vm->memory + base, BLOCK_SIZE);
vm->blk_vm_cbuf[s] = fresh;
vm->blk_vm_dirty[s] = 1;
blk_update((uint32_t) blk);
}
/* BLK-CONFIRM-FORMAT ( lbn -- ) : commit the low-level disk container
* format for the slot owning lbn. Must be called by the disk's owner
* (e.g. Artemis) only after classifying disk content as safe to touch —
* never on the path that halts for unrecognized content. Until this is
* called, all writes to that disk slot are refused by the block layer. */
void block_word_confirm_format(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk_subsys_confirm_format((uint32_t) blk) != BLK_OK) { vm->error = 1; return; }
}
/* RELOCATE-BLOCK ( home target -- ) : relocate home's content to target,
* redirecting all future access to home transparently through target
* from now on. See blk_subsys_relocate_block()'s own doc comment
* (block_subsystem.h) for the full contract -- this word performs no
* policy validation of its own (is target actually free? does the
* caller actually own it?), matching this project's "ACL owns policy,
* this is a mechanical primitive" direction. */
void block_word_relocate(VM *vm) {
if (vm->dsp < 1) { vm->error = 1; return; }
cell_t target = vm_pop(vm);
cell_t home = vm_pop(vm);
if (blk_subsys_relocate_block((uint32_t) home, (uint32_t) target) != BLK_OK) {
vm->error = 1;
}
}
/* SAVE-BUFFERS ( -- ) : sync and write all dirty blocks */
void block_word_save_buffers(VM *vm) {
blk_vm_flush_all(vm);
}
/* EMPTY-BUFFERS ( -- ) : invalidate all window slots then zero user blocks */
void block_word_empty_buffers(VM *vm) {
for (int i = 0; i < BLK_VM_SLOTS; i++) {
vm->blk_vm_lbn[i] = 0;
vm->blk_vm_cbuf[i] = NULL;
vm->blk_vm_dirty[i] = 0;
}
empty_all_buffers(vm);
}
/* FLUSH ( -- ) : save and invalidate all buffers */
void block_word_flush(VM *vm) {
blk_vm_flush_all(vm);
}
/* LOAD ( u -- ) : set SCR and (future: interpret block) */
void block_word_load(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) {
vm->error = 1;
return;
}
uint8_t *buf = blk_get_buffer((uint32_t) blk, 0);
if (!buf) {
vm->error = 1;
return;
}
set_scr(vm, blk);
/* Interpret the block content as Forth source */
/* Block is 1024 bytes, null-terminate it for interpretation */
char block_text[1025];
memcpy(block_text, buf, 1024);
block_text[1024] = '\0';
/* Interpret the block content */
vm_interpret(vm, block_text);
}
/* LIST ( u -- ) : set SCR and (optionally) print the block */
void block_word_list(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) {
vm->error = 1;
return;
}
uint8_t *buf = blk_get_buffer((uint32_t) blk, 0);
if (!buf) {
vm->error = 1;
return;
}
set_scr(vm, blk);
/* Print formatted block output with line numbers */
printf("\nBlock %ld\n", (long) blk);
/* FORTH blocks are typically 16 lines of 64 characters */
for (int line = 0; line < 16; line++) {
printf("%02d: ", line);
for (int col = 0; col < 64; col++) {
int idx = line * 64 + col;
char ch = (char) buf[idx];
/* Print printable characters, show spaces as-is */
if (ch >= 32 && ch < 127) {
putchar(ch);
} else if (ch == 0) {
/* Null bytes shown as spaces for readability */
putchar(' ');
} else {
/* Non-printable shown as '.' */
putchar('.');
}
}
printf("\n");
}
printf("\n");
}
/* THRU ( u1 u2 -- ) : LOAD each block from u1..u2 inclusive */
void block_word_thru(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t u2 = vm_pop(vm);
cell_t u1 = vm_pop(vm);
if (u1 > u2) {
cell_t t = u1;
u1 = u2;
u2 = t;
}
if (u1 == 0 || !blk_is_valid((uint32_t) u1) || !blk_is_valid((uint32_t) u2)) {
vm->error = 1;
return;
}
for (cell_t blk = u1; blk <= u2; ++blk) {
vm_push(vm, blk);
block_word_load(vm);
if (vm->error) return;
if (vm->abort_requested) return;
}
}
/* SCR ( -- addr ) : push VM address of SCR variable */
void block_word_scr(VM *vm) {
vm_push(vm, CELL(vm->scr_addr));
}
/* --> ( -- ) : continue interpretation on next sequential block */
void block_word_next_block(VM *vm) {
cell_t current_scr = vm_load_cell(vm, vm->scr_addr);
cell_t next_blk = current_scr + 1;
if (next_blk == 0 || !blk_is_valid((uint32_t) next_blk)) {
vm->error = 1;
return;
}
/* Get the next block's content */
uint8_t *buf = blk_get_buffer((uint32_t) next_blk, 0);
if (!buf) {
vm->error = 1;
return;
}
/* Update SCR to next block */
set_scr(vm, next_blk);
/* Interpret the next block's content line-by-line (same as LOAD) */
char block_text[1025];
memcpy(block_text, buf, 1024);
block_text[1024] = '\0';
char *p = block_text;
while (!vm->error && !vm->abort_requested && *p != '\0') {
char *nl = (char *)memchr(p, '\n', (size_t)(block_text + 1024 - p));
if (nl) {
*nl = '\0';
if (p != nl)
vm_interpret(vm, p);
p = nl + 1;
} else {
if (*p != '\0')
vm_interpret(vm, p);
break;
}
}
}
/* --- BMAPFMT block-ACL words (FABRIC-2.md §F.4/§H.6/§H.12 step 15) ----
* Raw C-primitive accessors, matching the word-level ACL system's own
* split: policy is composed in FORTH on top of these (a new capsule,
* §H.12 step 16), not here. block_num validated the same way BLOCK/
* BUFFER above already do. */
/* BLK-ACL-ALLOW@ ( block# -- allow ) */
static void block_word_acl_allow_fetch(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
uint8_t allow;
if (blk_acl_allow_get((uint32_t) blk, &allow) != BLK_OK) { vm->error = 1; return; }
vm_push(vm, (cell_t) allow);
}
/* BLK-ACL-ALLOW! ( allow block# -- ) */
static void block_word_acl_allow_store(VM *vm) {
if (vm->dsp < 1) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
cell_t allow = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
if (blk_acl_allow_set((uint32_t) blk, (uint8_t)(allow ? 1 : 0)) != BLK_OK) {
vm->error = 1;
}
}
/* BLK-ACL-TTL@ ( block# -- ttl ) */
static void block_word_acl_ttl_fetch(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
uint32_t ttl;
if (blk_acl_ttl_get((uint32_t) blk, &ttl) != BLK_OK) { vm->error = 1; return; }
vm_push(vm, (cell_t) ttl);
}
/* BLK-ACL-TTL! ( ttl block# -- ) */
static void block_word_acl_ttl_store(VM *vm) {
if (vm->dsp < 1) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
cell_t ttl = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
if (blk_acl_ttl_set((uint32_t) blk, (uint32_t) ttl) != BLK_OK) { vm->error = 1; }
}
/* BLK-OWNER@ ( block# -- fp ) : 8-byte owner fingerprint packed into one
* cell (cell_t is 8 bytes, int64_t -- see vm.h) -- raw bit reinterpretation,
* not a numeric value. Read-only from FORTH: only step 15's own list, no
* BLK-OWNER! -- setting ownership happens at a more controlled point
* (MINT/birth), in C, not exposed as a general FORTH write. */
static void block_word_owner_fetch(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
uint8_t fp[8];
if (blk_owner_fp_get((uint32_t) blk, fp) != BLK_OK) { vm->error = 1; return; }
cell_t packed;
memcpy(&packed, fp, sizeof(packed));
vm_push(vm, packed);
}
/* BLK-ATTACH ( dev-addr -- ok? ) : registers an already-open blkio_dev_t
* (its raw C pointer, passed as a cell -- the same "embed a pointer as a
* number in generated FORTH text" pattern VM-EXEC/the console relay
* already use) into the block subsystem's unified LBN address space via
* blk_subsys_attach_device(). This is the storage/persistence half of a
* USB drive attach -- Artemis's own domain -- split out from repl.c's
* attach loop so it can run as a message target (capsules/artemis/
* init.4th's HERA-BLK-ATTACH-REQ) instead of a direct C call, per Captain
* Bob's 2026-09-07 messaging-migration decision: Hera keeps polling and
* sig-checking, but no longer performs this step herself. dev-addr is
* trusted as a valid blkio_dev_t* from a live attach in progress -- there
* is no way to validate an arbitrary pointer from FORTH space, matching
* every other raw-pointer word this driver already exposes (e.g.
* dictionary_manipulation_words.c's >LINK/LINK>/CFA). */
static void block_word_blk_attach(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t dev_addr = vm_pop(vm);
struct blkio_dev *dev = (struct blkio_dev *)(uintptr_t)dev_addr;
int rc = blk_subsys_attach_device(dev);
vm_push(vm, (rc == BLK_OK) ? 1 : 0);
}
/* --- Registration ----------------------------------------------------- */
/*
* @brief Registers all block-related FORTH words
* @param vm Pointer to the VM instance
*/
void register_block_words(VM *vm) {
register_word(vm, "BLOCK", block_word_block);
register_word(vm, "BUFFER", block_word_buffer);
register_word(vm, "UPDATE", block_word_update);
register_word(vm, "BLK-CONFIRM-FORMAT", block_word_confirm_format);
register_word(vm, "RELOCATE-BLOCK", block_word_relocate);
register_word(vm, "SAVE-BUFFERS", block_word_save_buffers);
register_word(vm, "EMPTY-BUFFERS", block_word_empty_buffers);
register_word(vm, "FLUSH", block_word_flush);
register_word(vm, "LOAD", block_word_load);
register_word(vm, "LIST", block_word_list);
register_word(vm, "THRU", block_word_thru);
register_word(vm, "SCR", block_word_scr);
register_word(vm, "-->", block_word_next_block);
register_word(vm, "BLK-ACL-ALLOW@", block_word_acl_allow_fetch);
register_word(vm, "BLK-ACL-ALLOW!", block_word_acl_allow_store);
register_word(vm, "BLK-ACL-TTL@", block_word_acl_ttl_fetch);
register_word(vm, "BLK-ACL-TTL!", block_word_acl_ttl_store);
register_word(vm, "BLK-OWNER@", block_word_owner_fetch);
register_word(vm, "BLK-ATTACH", block_word_blk_attach);
}
+976
View File
@@ -0,0 +1,976 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "include/control_words.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
#include "../../include/vm.h"
#include <stddef.h>
#include <stdint.h>
/** @name Internal Runtime Functions
* Forward declarations for internal runtime helpers and control flow primitives
* @{
*/
/** Execute unconditional branch */
static void control_forth_branch(VM * vm);
/** Execute conditional branch based on top of stack */
static void control_forth_0branch(VM * vm);
/** Runtime handler for ?DO loops */
static void control_forth_runtime_qdo(VM * vm);
/** Runtime handler for DO loops */
static void control_forth_runtime_do(VM * vm);
/** Runtime handler for LOOP */
static void control_forth_runtime_loop(VM * vm);
/** Runtime handler for +LOOP */
static void control_forth_runtime_plus_loop(VM * vm);
/** Runtime handler for LEAVE */
static void control_forth_runtime_leave(VM * vm);
/** Get current loop index */
static void control_forth_I(VM * vm);
/** Get outer loop index */
static void control_forth_J(VM * vm);
/** Exit from current definition */
static void control_forth_EXIT(VM * vm);
/** @} */
/**
* @defgroup cf Control Flow Stack
* Compile-time control flow stack for tracking branch targets and loop
* structures. The types (cf_tag_t, cf_item_t, ControlFlowState) and the
* CF_STACK_MAX constant live in include/vm.h and the storage itself lives
* per-VM at vm->cf, so two VMs compiling colon definitions concurrently
* never share nesting state (see include/vm.h's ControlFlowState comment).
* @{
*/
static inline void cf_epoch_sync(VM *vm) {
if (!vm) return;
if (vm->cf.cf_last_mode == -999) {
vm->cf.cf_last_mode = vm->mode;
return;
}
if ((int)vm->mode != vm->cf.cf_last_mode) {
vm->cf.cf_sp = -1;
vm->cf.cf_last_mode = vm->mode;
log_message(LOG_DEBUG, "CF: reset (mode transition)");
}
}
static inline int cf_push_item(VM *vm, cf_tag_t tag, size_t mark) {
if (vm->cf.cf_sp + 1 >= CF_STACK_MAX) {
log_message(LOG_ERROR, "CF: overflow");
return 0;
}
++vm->cf.cf_sp;
vm->cf.cf_stack[vm->cf.cf_sp].tag = tag;
vm->cf.cf_stack[vm->cf.cf_sp].addr = mark;
return 1;
}
static inline int cf_pop_item(VM *vm, cf_item_t *out) {
if (vm->cf.cf_sp < 0) {
log_message(LOG_ERROR, "CF: underflow");
return 0;
}
if (out) {
*out = vm->cf.cf_stack[vm->cf.cf_sp];
}
--vm->cf.cf_sp;
return 1;
}
static inline int cf_peek_item(VM *vm, cf_item_t *out) {
if (vm->cf.cf_sp < 0) return 0;
if (out) {
*out = vm->cf.cf_stack[vm->cf.cf_sp];
}
return 1;
}
/** @} */
/* ===================== Low-level compile helpers ===================== */
static inline size_t emit_cell(VM *vm, cell_t value) {
if (vm->mode != MODE_COMPILE) {
vm_push(vm, value);
return (size_t) -1;
}
vm_align(vm);
cell_t *addr = (cell_t *) vm_allot(vm, sizeof(cell_t));
if (!addr) {
vm->error = 1;
log_message(LOG_ERROR, "emit_cell: out of space");
return (size_t) -1;
}
*addr = value;
return (size_t) ((uint8_t *) addr - vm->memory);
}
/**
* @defgroup runtime Runtime Branch Helpers
* Runtime support for control flow operations
* @details All functions operate on the per-step return IP (top of RS).
* All offsets are in BYTES.
* @{
*/
/**
* Runtime unconditional branch
* @param vm Virtual machine state
*/
static void control_forth_branch(VM *vm) {
if (vm->rsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "BRANCH: RSP underflow");
return;
}
cell_t *ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp];
cell_t rel = *ip;
ip = (cell_t *) ((uint8_t *) ip + (intptr_t) rel);
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
log_message(LOG_DEBUG, "BRANCH: +%ld bytes", (long) rel);
}
/* (0BRANCH) ( f -- ) */
static void control_forth_0branch(VM *vm) {
if (vm->rsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "0BRANCH: RSP underflow");
return;
}
if (vm->dsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "0BRANCH: DSP underflow");
return;
}
cell_t *ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp];
cell_t flag = vm_pop(vm);
cell_t rel = *ip;
if (flag == 0) {
ip = (cell_t *) ((uint8_t *) ip + (intptr_t) rel);
log_message(LOG_DEBUG, "0BRANCH: taken +%ld", (long) rel);
} else {
ip = ip + 1;
log_message(LOG_DEBUG, "0BRANCH: not taken");
}
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
}
/* (?DO) ( limit index -- ) */
static void control_forth_runtime_qdo(VM *vm) {
if (vm->rsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "?DO: RSP underflow");
return;
}
if (vm->dsp < 1) {
vm->error = 1;
log_message(LOG_ERROR, "?DO: DSP underflow");
return;
}
cell_t index = vm_pop(vm);
cell_t limit = vm_pop(vm);
cell_t *ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp];
cell_t rel = *ip;
if (index == limit) {
ip = (cell_t *) ((uint8_t *) ip + (intptr_t) rel);
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
log_message(LOG_DEBUG, "?DO: empty -> +%ld", (long) rel);
return;
}
/* enter loop at body (skip rel) and insert (limit,index) under IP */
ip = ip + 1;
if (vm->rsp + 2 >= STACK_SIZE) {
vm->error = 1;
log_message(LOG_ERROR, "?DO: RSTACK overflow");
return;
}
vm->return_stack[vm->rsp + 2] = (cell_t)(uintptr_t)
ip;
vm->return_stack[vm->rsp + 1] = index;
vm->return_stack[vm->rsp + 0] = limit;
vm->rsp += 2;
log_message(LOG_DEBUG, "?DO: enter (index=%ld limit=%ld)", (long) index, (long) limit);
}
/* (DO) ( limit index -- ) */
static void control_forth_runtime_do(VM *vm) {
if (vm->rsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "DO: RSP underflow");
return;
}
if (vm->dsp < 1) {
vm->error = 1;
log_message(LOG_ERROR, "DO: DSP underflow");
return;
}
cell_t index = vm_pop(vm);
cell_t limit = vm_pop(vm);
if (vm->rsp + 2 >= STACK_SIZE) {
vm->error = 1;
log_message(LOG_ERROR, "DO: RSTACK overflow");
return;
}
cell_t ip_cell = vm->return_stack[vm->rsp];
vm->return_stack[vm->rsp + 2] = ip_cell; /* move IP up */
vm->return_stack[vm->rsp + 1] = index;
vm->return_stack[vm->rsp + 0] = limit;
vm->rsp += 2;
log_message(LOG_DEBUG, "DO: enter (index=%ld limit=%ld)", (long) index, (long) limit);
}
/* (LOOP) */
static void control_forth_runtime_loop(VM *vm) {
if (vm->rsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "LOOP: missing loop frame");
return;
}
cell_t *ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp];
cell_t *idxp = &vm->return_stack[vm->rsp - 1];
cell_t *limp = &vm->return_stack[vm->rsp - 2];
(*idxp) += 1;
cell_t back = *ip; /* BYTES */
if (*idxp < *limp) {
ip = (cell_t *) ((uint8_t *) ip + (intptr_t) back);
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
log_message(LOG_DEBUG, "LOOP: continue (index=%ld)", (long) *idxp);
} else {
vm->rsp -= 2; /* drop INDEX,LIMIT */
ip = ip + 1; /* skip back-offset */
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
log_message(LOG_DEBUG, "LOOP: exit");
}
}
/* (+LOOP) ( n -- ) */
static void control_forth_runtime_plus_loop(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "+LOOP: DSP underflow");
return;
}
if (vm->rsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "+LOOP: missing loop frame");
return;
}
cell_t n = vm_pop(vm);
cell_t *ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp];
cell_t *idxp = &vm->return_stack[vm->rsp - 1];
cell_t *limp = &vm->return_stack[vm->rsp - 2];
cell_t newv = (*idxp) + n;
*idxp = newv;
cell_t back = *ip; /* BYTES */
int cont = (n >= 0) ? (newv < *limp) : (newv >= *limp);
if (cont) {
ip = (cell_t *) ((uint8_t *) ip + (intptr_t) back);
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
log_message(LOG_DEBUG, "+LOOP: continue (index=%ld)", (long) newv);
} else {
vm->rsp -= 2;
ip = ip + 1;
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
log_message(LOG_DEBUG, "+LOOP: exit");
}
}
/* (LEAVE) ( -- ) — force loop to exit at next LOOP/+LOOP */
static void control_forth_runtime_leave(VM *vm) {
if (vm->rsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "LEAVE: outside loop");
return;
}
vm->return_stack[vm->rsp - 1] = vm->return_stack[vm->rsp - 2]; /* index = limit */
log_message(LOG_DEBUG, "LEAVE: flagged exit");
}
/* UNLOOP ( -- ) — discard loop parameters from return stack */
static void control_forth_UNLOOP(VM *vm) {
if (vm->rsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "UNLOOP: outside loop (return stack underflow)");
return;
}
/* Return stack layout: ..., limit (rsp-2), index (rsp-1), ip (rsp) */
/* Move IP from rsp down to rsp-2, then decrement rsp by 2 */
vm->return_stack[vm->rsp - 2] = vm->return_stack[vm->rsp];
vm->rsp -= 2;
log_message(LOG_DEBUG, "UNLOOP: removed loop parameters (limit, index)");
}
/* I ( -- i ) */
static void control_forth_I(VM *vm) {
if (vm->rsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "I: outside DO loop");
return;
}
vm_push(vm, vm->return_stack[vm->rsp - 1]); /* INDEX */
}
/* J ( -- j ) — next-outer loop index */
static void control_forth_J(VM *vm) {
if (vm->rsp < 4) {
vm->error = 1;
log_message(LOG_ERROR, "J: needs nested DO loops");
return;
}
vm_push(vm, vm->return_stack[vm->rsp - 3]); /* outer INDEX */
}
/* EXIT — one-shot return from current colon definition (guarded) */
static void control_forth_EXIT(VM *vm) {
if (vm->rsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "EXIT: interpret-time use is invalid");
return;
}
vm->exit_colon = 1;
log_message(LOG_DEBUG, "EXIT: return from colon");
}
/* ===================== Compile-time words ===================== */
static void control_forth_if(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "IF: compile-only");
return;
}
vm_compile_call(vm, control_forth_0branch);
size_t lit = emit_cell(vm, 0);
if (!cf_push_item(vm, CF_IF, lit)) {
vm->error = 1;
return;
}
log_message(LOG_DEBUG, "IF: placeholder @ %zu", lit);
}
static void control_forth_else(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "ELSE: compile-only");
return;
}
cf_item_t it;
if (!cf_peek_item(vm, &it) || it.tag != CF_IF) {
vm->error = 1;
log_message(LOG_ERROR, "ELSE: missing IF");
return;
}
(void) cf_pop_item(vm, &it);
vm_compile_call(vm, control_forth_branch);
size_t new_lit = emit_cell(vm, 0);
cell_t off = (cell_t)((size_t) vm->here - it.addr);
*(cell_t *) (vm->memory + it.addr) = off;
if (!cf_push_item(vm, CF_ELSE, new_lit)) {
vm->error = 1;
return;
}
log_message(LOG_DEBUG, "ELSE: patched IF @ %zu -> +%ld; new lit @ %zu", it.addr, (long) off, new_lit);
}
static void control_forth_then(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "THEN: compile-only");
return;
}
cf_item_t it;
if (!cf_pop_item(vm, &it) || (it.tag != CF_IF && it.tag != CF_ELSE)) {
vm->error = 1;
log_message(LOG_ERROR, "THEN: unmatched");
return;
}
cell_t off = (cell_t)((size_t) vm->here - it.addr);
*(cell_t *) (vm->memory + it.addr) = off;
log_message(LOG_DEBUG, "THEN: patched lit @ %zu -> +%ld", it.addr, (long) off);
}
static void control_forth_begin(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "BEGIN: compile-only");
return;
}
if (!cf_push_item(vm, CF_BEGIN, vm->here)) {
vm->error = 1;
return;
}
log_message(LOG_DEBUG, "BEGIN: mark @ %zu", vm->here);
}
static void control_forth_until(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "UNTIL: compile-only");
return;
}
cf_item_t begin;
if (!cf_pop_item(vm, &begin) || begin.tag != CF_BEGIN) {
vm->error = 1;
log_message(LOG_ERROR, "UNTIL: missing BEGIN");
return;
}
vm_compile_call(vm, control_forth_0branch);
cell_t back = (cell_t)((cell_t) begin.addr - (cell_t) vm->here);
(void) emit_cell(vm, back);
log_message(LOG_DEBUG, "UNTIL: back -> %zu (%ld bytes)", begin.addr, (long) back);
}
static void control_forth_again(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "AGAIN: compile-only");
return;
}
cf_item_t begin;
if (!cf_pop_item(vm, &begin) || begin.tag != CF_BEGIN) {
vm->error = 1;
log_message(LOG_ERROR, "AGAIN: missing BEGIN");
return;
}
vm_compile_call(vm, control_forth_branch);
cell_t back = (cell_t)((cell_t) begin.addr - (cell_t) vm->here);
(void) emit_cell(vm, back);
log_message(LOG_DEBUG, "AGAIN: back -> %zu (%ld bytes)", begin.addr, (long) back);
}
static void control_forth_while(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "WHILE: compile-only");
return;
}
cf_item_t b;
if (!cf_peek_item(vm, &b) || b.tag != CF_BEGIN) {
vm->error = 1;
log_message(LOG_ERROR, "WHILE: needs BEGIN");
return;
}
vm_compile_call(vm, control_forth_0branch);
size_t lit = emit_cell(vm, 0);
if (!cf_push_item(vm, CF_WHILE, lit)) {
vm->error = 1;
return;
}
log_message(LOG_DEBUG, "WHILE: placeholder @ %zu", lit);
}
static void control_forth_repeat(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "REPEAT: compile-only");
return;
}
cf_item_t w, b;
if (!cf_pop_item(vm, &w) || w.tag != CF_WHILE) {
vm->error = 1;
log_message(LOG_ERROR, "REPEAT: missing WHILE");
return;
}
if (!cf_pop_item(vm, &b) || b.tag != CF_BEGIN) {
vm->error = 1;
log_message(LOG_ERROR, "REPEAT: missing BEGIN");
return;
}
vm_compile_call(vm, control_forth_branch);
cell_t back = (cell_t)((cell_t) b.addr - (cell_t) vm->here);
(void) emit_cell(vm, back);
cell_t fwd = (cell_t)((size_t) vm->here - w.addr);
*(cell_t *) (vm->memory + w.addr) = fwd;
log_message(LOG_DEBUG, "REPEAT: WHILE @ %zu -> +%ld; back=%ld to %zu", w.addr, (long) fwd, (long) back, b.addr);
}
/* ?DO ( limit index -- ) compile */
static void control_forth_qdo(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "?DO: compile-only");
return;
}
vm_compile_call(vm, control_forth_runtime_qdo);
size_t fwd_lit = emit_cell(vm, 0);
if (!cf_push_item(vm, CF_DO, vm->here)) {
vm->error = 1;
return;
} /* back target for LOOP */
if (!cf_push_item(vm, CF_WHILE, fwd_lit)) {
vm->error = 1;
return;
} /* forward to loop-end */
vm->cf.leave_mark_stack[++vm->cf.leave_mark_sp] = vm->cf.leave_sp;
log_message(LOG_DEBUG, "?DO: fwd lit @ %zu; back mark=%d", fwd_lit, vm->cf.leave_mark_stack[vm->cf.leave_mark_sp]);
}
/* DO ( limit index -- ) compile */
static void control_forth_do(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "DO: compile-only");
return;
}
vm_compile_call(vm, control_forth_runtime_do);
if (!cf_push_item(vm, CF_DO, vm->here)) {
vm->error = 1;
return;
}
vm->cf.leave_mark_stack[++vm->cf.leave_mark_sp] = vm->cf.leave_sp;
log_message(LOG_DEBUG, "DO: mark @ %zu; leave_mark=%d", vm->here, vm->cf.leave_mark_stack[vm->cf.leave_mark_sp]);
}
/* LEAVE — compile runtime LEAVE plus BRANCH <placeholder>, collect patch site */
static void control_forth_leave(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "LEAVE: compile-only");
return;
}
/* verify inside DO */
int seen_do = 0;
for (int i = vm->cf.cf_sp; i >= 0; --i) {
if (vm->cf.cf_stack[i].tag == CF_DO) {
seen_do = 1;
break;
}
}
if (!seen_do || vm->cf.leave_mark_sp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "LEAVE: needs DO");
return;
}
vm_compile_call(vm, control_forth_runtime_leave);
vm_compile_call(vm, control_forth_branch);
size_t lit = emit_cell(vm, 0);
if (vm->cf.leave_sp + 1 >= CF_STACK_MAX) {
vm->error = 1;
log_message(LOG_ERROR, "LEAVE: too many sites");
return;
}
vm->cf.leave_addrs[++vm->cf.leave_sp] = lit;
log_message(LOG_DEBUG, "LEAVE: site lit @ %zu (leave_sp=%d)", lit, vm->cf.leave_sp);
}
/* LOOP — compile runtime LOOP + backoffset; patch ?DO fwd and LEAVE sites */
static void control_forth_loop(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "LOOP: compile-only");
return;
}
/* Optional ?DO forward */
cf_item_t maybe_qdo;
int have_qdo = 0;
cf_item_t top;
if (cf_peek_item(vm, &top) && top.tag == CF_WHILE) {
(void) cf_pop_item(vm, &maybe_qdo);
have_qdo = 1;
}
/* Required DO back mark */
cf_item_t do_mark;
if (!cf_pop_item(vm, &do_mark) || do_mark.tag != CF_DO) {
vm->error = 1;
log_message(LOG_ERROR, "LOOP: missing DO");
return;
}
vm_compile_call(vm, control_forth_runtime_loop);
cell_t back = (cell_t)((cell_t) do_mark.addr - (cell_t) vm->here);
(void) emit_cell(vm, back);
if (have_qdo) {
cell_t fwd = (cell_t)((size_t) vm->here - maybe_qdo.addr);
*(cell_t *) (vm->memory + maybe_qdo.addr) = fwd;
log_message(LOG_DEBUG, "LOOP: patched ?DO @ %zu -> +%ld", maybe_qdo.addr, (long) fwd);
}
if (vm->cf.leave_mark_sp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "LOOP: LEAVE mark underflow");
return;
}
int mark = vm->cf.leave_mark_stack[vm->cf.leave_mark_sp--];
for (int i = vm->cf.leave_sp; i > mark; --i) {
size_t addr = vm->cf.leave_addrs[i];
cell_t fwd = (cell_t)((size_t) vm->here - addr);
*(cell_t *) (vm->memory + addr) = fwd;
log_message(LOG_DEBUG, "LEAVE: patched @ %zu -> +%ld", addr, (long) fwd);
}
vm->cf.leave_sp = mark;
log_message(LOG_DEBUG, "LOOP: back -> %zu (%ld bytes)", do_mark.addr, (long) back);
}
/* +LOOP — same as LOOP but with runtime +LOOP */
static void control_forth_plus_loop(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "+LOOP: compile-only");
return;
}
cf_item_t maybe_qdo;
int have_qdo = 0;
cf_item_t top;
if (cf_peek_item(vm, &top) && top.tag == CF_WHILE) {
(void) cf_pop_item(vm, &maybe_qdo);
have_qdo = 1;
}
cf_item_t do_mark;
if (!cf_pop_item(vm, &do_mark) || do_mark.tag != CF_DO) {
vm->error = 1;
log_message(LOG_ERROR, "+LOOP: missing DO");
return;
}
vm_compile_call(vm, control_forth_runtime_plus_loop);
cell_t back = (cell_t)((cell_t) do_mark.addr - (cell_t) vm->here);
(void) emit_cell(vm, back);
if (have_qdo) {
cell_t fwd = (cell_t)((size_t) vm->here - maybe_qdo.addr);
*(cell_t *) (vm->memory + maybe_qdo.addr) = fwd;
log_message(LOG_DEBUG, "+LOOP: patched ?DO @ %zu -> +%ld", maybe_qdo.addr, (long) fwd);
}
if (vm->cf.leave_mark_sp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "+LOOP: LEAVE mark underflow");
return;
}
int mark = vm->cf.leave_mark_stack[vm->cf.leave_mark_sp--];
for (int i = vm->cf.leave_sp; i > mark; --i) {
size_t addr = vm->cf.leave_addrs[i];
cell_t fwd = (cell_t)((size_t) vm->here - addr);
*(cell_t *) (vm->memory + addr) = fwd;
log_message(LOG_DEBUG, "LEAVE: patched @ %zu -> +%ld", addr, (long) fwd);
}
vm->cf.leave_sp = mark;
log_message(LOG_DEBUG, "+LOOP: back -> %zu (%ld bytes)", do_mark.addr, (long) back);
}
/* ===================== CASE/OF/ENDOF/ENDCASE ===================== */
/* Standard FORTH CASE statement:
* n CASE
* val1 OF code1 ENDOF
* val2 OF code2 ENDOF
* default-code
* ENDCASE
*
* Compile-time behavior:
* - CASE: Push CF_CASE marker, record endof_sp for ENDCASE patching
* - OF: Compile OVER = 0BRANCH <endof> DROP, push CF_OF with branch addr
* - ENDOF: Patch OF's forward branch, compile BRANCH <endcase>, save for ENDCASE
* - ENDCASE: Compile DROP, patch all ENDOF branches to here
*/
/* CASE ( n -- n ) compile-time: mark start of case statement */
static void control_forth_case(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "CASE: compile-only");
return;
}
if (!cf_push_item(vm, CF_CASE, 0)) {
vm->error = 1;
return;
}
/* Record current endof_sp so ENDCASE knows which branches to patch */
if (vm->cf.endof_mark_sp + 1 >= CF_STACK_MAX) {
vm->error = 1;
log_message(LOG_ERROR, "CASE: nesting overflow");
return;
}
vm->cf.endof_mark_stack[++vm->cf.endof_mark_sp] = vm->cf.endof_sp;
log_message(LOG_DEBUG, "CASE: mark (endof_mark=%d)", vm->cf.endof_mark_stack[vm->cf.endof_mark_sp]);
}
/* OF ( n1 n2 -- | n1 ) compile-time: compare and branch */
static void control_forth_of(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "OF: compile-only");
return;
}
/* Verify inside CASE */
int seen_case = 0;
for (int i = vm->cf.cf_sp; i >= 0; --i) {
if (vm->cf.cf_stack[i].tag == CF_CASE) {
seen_case = 1;
break;
}
}
if (!seen_case) {
vm->error = 1;
log_message(LOG_ERROR, "OF: needs CASE");
return;
}
/* Compile: OVER = 0BRANCH <endof> DROP */
/* We need to compile calls to OVER and = */
DictEntry *over_entry = vm_find_word(vm, "OVER", 4);
DictEntry *eq_entry = vm_find_word(vm, "=", 1);
DictEntry *drop_entry = vm_find_word(vm, "DROP", 4);
if (!over_entry || !eq_entry || !drop_entry) {
vm->error = 1;
log_message(LOG_ERROR, "OF: missing OVER, =, or DROP");
return;
}
vm_compile_call(vm, over_entry->func);
vm_compile_call(vm, eq_entry->func);
vm_compile_call(vm, control_forth_0branch);
size_t of_branch = emit_cell(vm, 0); /* Placeholder for ENDOF */
vm_compile_call(vm, drop_entry->func);
if (!cf_push_item(vm, CF_OF, of_branch)) {
vm->error = 1;
return;
}
log_message(LOG_DEBUG, "OF: branch placeholder @ %zu", of_branch);
}
/* ENDOF ( -- ) compile-time: end OF clause, jump to ENDCASE */
static void control_forth_endof(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "ENDOF: compile-only");
return;
}
/* Pop CF_OF and patch its forward branch */
cf_item_t of_item;
if (!cf_pop_item(vm, &of_item) || of_item.tag != CF_OF) {
vm->error = 1;
log_message(LOG_ERROR, "ENDOF: missing OF");
return;
}
/* Compile BRANCH to ENDCASE (placeholder) */
vm_compile_call(vm, control_forth_branch);
size_t endcase_branch = emit_cell(vm, 0);
/* Patch OF's 0BRANCH to jump here (after the BRANCH) */
cell_t off = (cell_t)((size_t) vm->here - of_item.addr);
*(cell_t *) (vm->memory + of_item.addr) = off;
/* Save ENDOF's branch for ENDCASE patching */
if (vm->cf.endof_sp + 1 >= CF_STACK_MAX) {
vm->error = 1;
log_message(LOG_ERROR, "ENDOF: too many clauses");
return;
}
vm->cf.endof_addrs[++vm->cf.endof_sp] = endcase_branch;
log_message(LOG_DEBUG, "ENDOF: patched OF @ %zu -> +%ld; endcase branch @ %zu",
of_item.addr, (long) off, endcase_branch);
}
/* ENDCASE ( n -- ) compile-time: end case, drop selector, patch all ENDOFs */
static void control_forth_endcase(VM *vm) {
cf_epoch_sync(vm);
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "ENDCASE: compile-only");
return;
}
/* Pop CF_CASE marker */
cf_item_t case_item;
if (!cf_pop_item(vm, &case_item) || case_item.tag != CF_CASE) {
vm->error = 1;
log_message(LOG_ERROR, "ENDCASE: missing CASE");
return;
}
/* Compile DROP to discard the selector */
DictEntry *drop_entry = vm_find_word(vm, "DROP", 4);
if (!drop_entry) {
vm->error = 1;
log_message(LOG_ERROR, "ENDCASE: missing DROP");
return;
}
vm_compile_call(vm, drop_entry->func);
/* Patch all ENDOF branches to here */
if (vm->cf.endof_mark_sp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "ENDCASE: mark underflow");
return;
}
int mark = vm->cf.endof_mark_stack[vm->cf.endof_mark_sp--];
for (int i = vm->cf.endof_sp; i > mark; --i) {
size_t addr = vm->cf.endof_addrs[i];
cell_t fwd = (cell_t)((size_t) vm->here - addr);
*(cell_t *) (vm->memory + addr) = fwd;
log_message(LOG_DEBUG, "ENDCASE: patched ENDOF @ %zu -> +%ld", addr, (long) fwd);
}
vm->cf.endof_sp = mark;
log_message(LOG_DEBUG, "ENDCASE: complete");
}
/* ===================== Registration ===================== */
void register_control_words(VM *vm) {
if (!vm) return;
/* Internal branches & runtimes */
register_word(vm, "(BRANCH)", control_forth_branch);
register_word(vm, "(0BRANCH)", control_forth_0branch);
register_word(vm, "(?DO)", control_forth_runtime_qdo);
register_word(vm, "(DO)", control_forth_runtime_do);
register_word(vm, "(LOOP)", control_forth_runtime_loop);
register_word(vm, "(+LOOP)", control_forth_runtime_plus_loop);
register_word(vm, "(LEAVE)", control_forth_runtime_leave);
/* IF/ELSE/THEN */
register_word(vm, "IF", control_forth_if);
vm_make_immediate(vm);
register_word(vm, "ELSE", control_forth_else);
vm_make_immediate(vm);
register_word(vm, "THEN", control_forth_then);
vm_make_immediate(vm);
/* BEGIN/WHILE/REPEAT/AGAIN/UNTIL */
register_word(vm, "BEGIN", control_forth_begin);
vm_make_immediate(vm);
register_word(vm, "WHILE", control_forth_while);
vm_make_immediate(vm);
register_word(vm, "REPEAT", control_forth_repeat);
vm_make_immediate(vm);
register_word(vm, "AGAIN", control_forth_again);
vm_make_immediate(vm);
register_word(vm, "UNTIL", control_forth_until);
vm_make_immediate(vm);
/* DO/?DO/LOOP/+LOOP/LEAVE */
register_word(vm, "?DO", control_forth_qdo);
vm_make_immediate(vm);
register_word(vm, "DO", control_forth_do);
vm_make_immediate(vm);
register_word(vm, "LOOP", control_forth_loop);
vm_make_immediate(vm);
register_word(vm, "+LOOP", control_forth_plus_loop);
vm_make_immediate(vm);
register_word(vm, "LEAVE", control_forth_leave);
vm_make_immediate(vm);
/* Loop indices, UNLOOP & EXIT */
register_word(vm, "I", control_forth_I);
register_word(vm, "J", control_forth_J);
register_word(vm, "UNLOOP", control_forth_UNLOOP);
register_word(vm, "EXIT", control_forth_EXIT);
/* CASE/OF/ENDOF/ENDCASE */
register_word(vm, "CASE", control_forth_case);
vm_make_immediate(vm);
register_word(vm, "OF", control_forth_of);
vm_make_immediate(vm);
register_word(vm, "ENDOF", control_forth_endof);
vm_make_immediate(vm);
register_word(vm, "ENDCASE", control_forth_endcase);
vm_make_immediate(vm);
}
+208
View File
@@ -0,0 +1,208 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/*
* DEFER / IS / DEFER@ primitives — late-binding deferred words.
*
* DEFER name ( -- ) Create a deferred word, data field = 0 (unset).
* IS name ( xt -- ) Store xt into the data field of a DEFER word.
* DEFER@ name ( -- xt ) Fetch the current action xt of a DEFER word.
*
* When a deferred word executes, defer_runtime reads its data field
* (a raw DictEntry*) and calls that entry's func directly. This is
* essential because the inner interpreter stores DictEntry* pointers
* in compiled bodies at compile time — redefining a colon word after
* compilation does not update callers. DEFER/IS gives late binding
* without any name-lookup overhead at execution time.
*
* IS enforces that the named word was created by DEFER (func ==
* defer_runtime). Attempting IS on any other word sets vm->error.
*/
#include "include/defer_words.h"
#include "../../include/vm.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
#include "../../include/dictionary_management.h"
#include <stdint.h>
/* ── defer_runtime ─────────────────────────────────────────────────────
* The func stored in every DEFER-created word.
* Data field = 0 means unset → error. Non-zero = DictEntry* to call.
*/
static void defer_runtime(VM *vm)
{
if (!vm || !vm->current_executing_entry) { if (vm) vm->error = 1; return; }
DictEntry *self = vm->current_executing_entry;
cell_t *df = vm_dictionary_get_data_field(self);
if (!df || *df == 0)
{
log_message(LOG_ERROR, "DEFER runtime: no target set for '%.*s'",
(int)self->name_len, self->name);
vm->error = 1;
return;
}
DictEntry *target = (DictEntry *)(uintptr_t)(*df);
if (!target || !target->func)
{
log_message(LOG_ERROR, "DEFER runtime: invalid target for '%.*s'",
(int)self->name_len, self->name);
vm->error = 1;
return;
}
vm->current_executing_entry = target;
target->func(vm);
}
/* ── DEFER ─────────────────────────────────────────────────────────────
* DEFER name ( -- )
* Creates a new dictionary entry with defer_runtime as its func.
* Data field initialised to 0 (unset). Calling the word before IS
* has been used will set vm->error.
*/
static void word_defer(VM *vm)
{
char name[WORD_NAME_MAX + 1];
int len = vm_parse_word(vm, name, sizeof(name));
if (len <= 0)
{
log_message(LOG_ERROR, "DEFER: missing name");
vm->error = 1;
return;
}
DictEntry *de = vm_create_word(vm, name, (size_t)len, defer_runtime);
if (!de)
{
log_message(LOG_ERROR, "DEFER: vm_create_word failed for '%.*s'", len, name);
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(de);
if (!df)
{
log_message(LOG_ERROR, "DEFER: no data field for '%.*s'", len, name);
vm->error = 1;
return;
}
*df = 0; /* unset — defer_runtime will error if called before IS */
log_message(LOG_DEBUG, "DEFER: created '%.*s' (unset)", len, name);
}
/* ── IS ────────────────────────────────────────────────────────────────
* IS name ( xt -- )
* Pops an execution token (DictEntry*) from the stack, parses the next
* word from input, and stores the xt into that word's data field.
* The named word MUST have been created by DEFER; any other word causes
* an error. This prevents corrupting non-DEFER words' memory.
*/
static void word_is(VM *vm)
{
if (vm->dsp < 0)
{
log_message(LOG_ERROR, "IS: stack underflow");
vm->error = 1;
return;
}
cell_t xt = vm_pop(vm);
char name[WORD_NAME_MAX + 1];
int len = vm_parse_word(vm, name, sizeof(name));
if (len <= 0)
{
log_message(LOG_ERROR, "IS: missing name");
vm->error = 1;
return;
}
DictEntry *de = vm_find_word(vm, name, (size_t)len);
if (!de)
{
log_message(LOG_ERROR, "IS: word '%.*s' not found", len, name);
vm->error = 1;
return;
}
if (de->func != defer_runtime)
{
log_message(LOG_ERROR, "IS: '%.*s' was not created by DEFER", len, name);
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(de);
if (!df)
{
log_message(LOG_ERROR, "IS: no data field for '%.*s'", len, name);
vm->error = 1;
return;
}
*df = xt;
log_message(LOG_DEBUG, "IS: stored xt into '%.*s'", len, name);
}
/* ── DEFER@ ────────────────────────────────────────────────────────────
* DEFER@ name ( -- xt )
* Parses the next word from input and pushes its current action xt.
* The named word must have been created by DEFER.
*/
static void word_defer_fetch(VM *vm)
{
char name[WORD_NAME_MAX + 1];
int len = vm_parse_word(vm, name, sizeof(name));
if (len <= 0)
{
log_message(LOG_ERROR, "DEFER@: missing name");
vm->error = 1;
return;
}
DictEntry *de = vm_find_word(vm, name, (size_t)len);
if (!de)
{
log_message(LOG_ERROR, "DEFER@: word '%.*s' not found", len, name);
vm->error = 1;
return;
}
if (de->func != defer_runtime)
{
log_message(LOG_ERROR, "DEFER@: '%.*s' was not created by DEFER", len, name);
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(de);
if (!df)
{
log_message(LOG_ERROR, "DEFER@: no data field for '%.*s'", len, name);
vm->error = 1;
return;
}
vm_push(vm, *df);
log_message(LOG_DEBUG, "DEFER@: fetched xt from '%.*s'", len, name);
}
/* ── Registration ──────────────────────────────────────────────────────*/
void register_defer_words(VM *vm)
{
register_word(vm, "DEFER", word_defer);
register_word(vm, "IS", word_is);
register_word(vm, "DEFER@", word_defer_fetch);
}
+793
View File
@@ -0,0 +1,793 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "include/defining_words.h"
#include "../../include/vm.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
#include "../../include/profiler.h"
#include "../../include/physics_metadata.h"
#include "../../include/platform_time.h"
#include "../../include/physics_pipelining_metrics.h"
#include <string.h>
#include <stdlib.h> /* malloc/free */
/* ───── Forward declarations so registration sees these symbols ───── */
static void defining_runtime_create(VM * vm);
static void defining_runtime_constant(VM * vm);
static void defining_runtime_variable(VM * vm);
static void defining_runtime_lit(VM * vm);
static void defining_runtime_dodoes(VM * vm);
static void defining_runtime_does_rt(VM * vm);
static void defining_word_colon(VM * vm);
static void defining_word_semicolon(VM * vm);
static void defining_word_create(VM * vm);
static void defining_word_constant(VM * vm);
static void defining_word_variable(VM * vm);
static void defining_word_left_bracket(VM * vm);
static void defining_word_right_bracket(VM * vm);
static void defining_word_state(VM * vm);
static void defining_word_compile(VM * vm);
static void defining_word_bracket_compile(VM * vm);
static void defining_word_literal(VM * vm);
static void defining_word_does(VM * vm);
static void defining_word_immediate(VM * vm);
static void dictionary_word_forget(VM * vm);
static void dictionary_word_fence(VM * vm);
/* ───────────────────────────── Runtimes ───────────────────────────── */
/* CONSTANT runtime: ( -- n ) Push stored value from header DF cell */
/**
* @brief Runtime behavior for CONSTANT words
*
* Pushes the constant value stored in the word's data field onto the stack.
* Stack effect: ( -- n )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_constant(VM *vm) {
if (!vm) return;
DictEntry *entry = vm->current_executing_entry;
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
vm_push(vm, *df);
log_message(LOG_DEBUG, "CONSTANT runtime: pushed %ld for %.*s",
(long) *df, entry->name_len, entry->name);
}
/* VARIABLE runtime: ( -- addr ) Push VM offset stored in header DF cell */
/**
* @brief Runtime behavior for VARIABLE words
*
* Pushes the variable's address (VM offset) stored in the word's data field.
* Stack effect: ( -- addr )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_variable(VM *vm) {
if (!vm) return;
DictEntry *entry = vm->current_executing_entry;
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
vm_push(vm, *df);
log_message(LOG_DEBUG, "VARIABLE runtime: pushed VM addr %ld for %.*s",
(long) *df, entry->name_len, entry->name);
}
/* CREATE runtime: ( -- addr ) Push DFA (VM byte offset) captured at CREATE */
/**
* @brief Runtime behavior for words defined by CREATE
*
* Pushes the word's data field address (DFA) captured at CREATE time.
* Stack effect: ( -- addr )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_create(VM *vm) {
if (!vm) return;
DictEntry *entry = vm->current_executing_entry;
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
vaddr_t dfa = (vaddr_t)(uint64_t)(*df);
if (!vm_addr_ok(vm, dfa, sizeof(cell_t))) {
vm->error = 1;
return;
}
vm_push(vm, CELL(dfa));
log_message(LOG_DEBUG, "CREATE runtime: pushed DFA %ld for %.*s",
(long) dfa, entry->name_len, entry->name);
}
/* LIT runtime: ( -- n ) — fetch next cell from threaded code via R-stack IP */
/**
* @brief Runtime behavior for literal values in threaded code
*
* Fetches the next cell from threaded code via IP and pushes it.
* Stack effect: ( -- n )
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_lit(VM *vm) {
if (!vm) return;
if (vm->rsp < 0) {
vm->error = 1;
return;
}
cell_t *rip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp]; /* peek IP */
if (!rip) {
vm->error = 1;
return;
}
cell_t value = *rip; /* read literal cell */
vm_push(vm, value);
rip++; /* advance IP to after literal */
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
rip; /* write-back */
log_message(LOG_DEBUG, "LIT: pushed %ld", (long) value);
}
/* DODOES runtime: when a created word runs, push PFA+8 (user data addr) then execute DOES>-body
PFA layout: [body_vaddr][user_data...]
We push the address of user_data, then execute body_vaddr code. */
/**
* @brief Runtime behavior for words defined with DOES>
*
* Pushes the address of user data area then executes the DOES> body code.
* PFA layout: [body_vaddr][user_data...]
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_dodoes(VM *vm) {
if (!vm) return;
DictEntry *self = vm->current_executing_entry;
if (!self) {
vm->error = 1;
log_message(LOG_ERROR, "DODOES: no current entry");
return;
}
cell_t *df = vm_dictionary_get_data_field(self);
if (!df) {
vm->error = 1;
return;
}
vaddr_t pfa = (vaddr_t)(uint64_t)(*df);
/* PFA[0] holds vaddr of DOES>-body */
if (!vm_addr_ok(vm, pfa, sizeof(cell_t))) {
vm->error = 1;
log_message(LOG_ERROR, "DODOES: invalid PFA address %zu", (size_t) pfa);
return;
}
vaddr_t body_vaddr = (vaddr_t)(uint64_t)(*(cell_t *) vm_ptr(vm, pfa));
log_message(LOG_DEBUG, "DODOES: body_vaddr=%zu (loaded from PFA[0] at %zu)", (size_t) body_vaddr, (size_t) pfa);
if (!vm_addr_ok(vm, body_vaddr, sizeof(cell_t))) {
vm->error = 1;
log_message(LOG_ERROR, "DODOES: invalid body_vaddr %zu", (size_t) body_vaddr);
return;
}
/* Push address of user data (PFA + sizeof(cell_t)) per DOES> semantics */
vaddr_t user_data_addr = pfa + sizeof(cell_t);
vm_push(vm, (cell_t) user_data_addr);
/* Execute like a colon word until EXIT pops the frame */
int base_rsp = vm->rsp;
cell_t *ip = (cell_t *) vm_ptr(vm, body_vaddr);
vm->rsp++;
if (vm->rsp >= STACK_SIZE) {
vm->error = 1;
vm->rsp = base_rsp;
return;
}
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
ip;
while (!vm->error && !vm->abort_requested && !vm->exit_colon && vm->rsp >= base_rsp + 1) {
cell_t *cur_ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp];
DictEntry *entry = (DictEntry *) (uintptr_t)(*cur_ip++);
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)
cur_ip;
if (!entry || !entry->func) {
vm->error = 1;
break;
}
vm->current_executing_entry = entry;
physics_execution_heat_increment(entry);
profiler_word_count(entry);
profiler_word_enter(entry);
entry->func(vm);
physics_metadata_touch(entry, entry->execution_heat, sf_monotonic_ns());
profiler_word_exit(entry);
}
/* Handle EXIT from the DOES> body */
if (vm->exit_colon) {
vm->rsp = base_rsp;
vm->exit_colon = 0;
}
/* ABORT already reset rsp (reset_vm_state(), called synchronously by
* system_word_abort() before this loop even notices the flag) --
* restoring it to base_rsp here would partially undo that reset.
* Only clamp back up to base_rsp on a normal/EXIT completion. */
if (!vm->abort_requested && vm->rsp < base_rsp) vm->rsp = base_rsp;
}
/* does_rt: runs inside the defining word at DOES> time.
Converts the just-created child so its runtime is DODOES and
records the DOES>-body start address at the child's PFA[0].
NOTE: PFA already contains user data from , or other compile-time words.
We need to build a new PFA structure: [body_vaddr][old_user_data...]
and update the child's DF to point to this new PFA. */
/**
* @brief Helper routine executed at DOES> time
*
* Converts the just-created child word to use DODOES runtime behavior
* and records the DOES> body address in the child's PFA.
*
* @param vm Pointer to the VM context
*/
static void defining_runtime_does_rt(VM *vm) {
if (!vm) return;
DictEntry *child = vm->latest;
if (!child) {
vm->error = 1;
log_message(LOG_ERROR, "DOES>: no child to patch");
return;
}
/* IP (top R-stack) points at the next entry (EXIT we compile below); body starts after that */
if (vm->rsp < 0) {
vm->error = 1;
return;
}
cell_t *ip = (cell_t *) (uintptr_t) vm->return_stack[vm->rsp]; /* IP is a real pointer, not vaddr */
cell_t *body_ip = ip + 1; /* first entry AFTER EXIT */
vaddr_t body_vaddr = (vaddr_t)((uint8_t *) body_ip - vm->memory);
cell_t *df = vm_dictionary_get_data_field(child);
if (!df) {
vm->error = 1;
return;
}
vaddr_t old_pfa = (vaddr_t)(uint64_t)(*df);
/* Calculate how much user data was allocated between old PFA and current HERE */
vaddr_t current_here = (vaddr_t) vm->here;
size_t user_data_size = (old_pfa < current_here) ? (current_here - old_pfa) : 0;
/* Allocate new PFA: [body_vaddr][user_data...] */
vm_align(vm);
vaddr_t new_pfa = (vaddr_t) vm->here;
/* Allocate space for body_vaddr pointer */
cell_t *body_ptr = (cell_t *) vm_allot(vm, sizeof(cell_t));
if (!body_ptr) {
vm->error = 1;
return;
}
*body_ptr = (cell_t) body_vaddr;
/* Copy user data if any was allocated */
if (user_data_size > 0) {
void *user_data_dest = vm_allot(vm, user_data_size);
if (!user_data_dest) {
vm->error = 1;
return;
}
if (vm_addr_ok(vm, old_pfa, user_data_size)) {
memcpy(user_data_dest, vm_ptr(vm, old_pfa), user_data_size);
}
}
/* Update child's DF to point to new PFA */
*df = (cell_t) new_pfa;
child->func = defining_runtime_dodoes;
}
/* ───────────────────────────── Defining words ─────────────────────── */
/* CREATE ( "name" -- ) — 79: does NOT allocate data; captures cell-aligned HERE as DFA */
static void defining_word_create(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
DictEntry *entry = vm_create_word(vm, namebuf, (size_t) nlen, defining_runtime_create);
if (!entry) {
vm->error = 1;
return;
}
vm_align(vm); /* align HERE for cell data */
vaddr_t dfa = (vaddr_t) vm->here;
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
*df = (cell_t)(int64_t)
dfa;
log_message(LOG_DEBUG, "CREATE: '%.*s' DFA=%ld (HERE=%ld)",
nlen, namebuf, (long) dfa, (long) vm->here);
}
/* : ( "name" -- ) begin colon definition — IMMEDIATE */
static void defining_word_colon(VM *vm) {
if (!vm) return;
/* 79: nested colon definitions are illegal */
if (vm->mode == MODE_COMPILE) {
log_message(LOG_ERROR, "Nested ':' inside a definition is not allowed");
vm->error = 1;
return;
}
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
vm_enter_compile_mode(vm, namebuf, (size_t) nlen);
log_message(LOG_DEBUG, ": Started definition of '%.*s'", nlen, namebuf);
}
/* ; ( -- ) end colon definition — IMMEDIATE (compiles EXIT in vm_exit_compile_mode) */
static void defining_word_semicolon(VM *vm) {
if (!vm) return;
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
return;
}
vm_exit_compile_mode(vm);
}
/* CONSTANT ( n "name" -- ) */
static void defining_word_constant(VM *vm) {
if (!vm) return;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t value = vm_pop(vm);
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
DictEntry *entry = vm_create_word(vm, namebuf, (size_t) nlen, defining_runtime_constant);
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
*df = value;
log_message(LOG_DEBUG, "CONSTANT: '%.*s' = %ld", nlen, namebuf, (long) value);
}
/* VARIABLE ( "name" -- ) allocate one cell in VM and store its addr in header DF */
static void defining_word_variable(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
vm_align(vm);
vaddr_t addr = (vaddr_t) vm->here;
void *p = vm_allot(vm, sizeof(cell_t));
if (!p) {
vm->error = 1;
return;
}
*(cell_t *) p = 0;
DictEntry *entry = vm_create_word(vm, namebuf, (size_t) nlen, defining_runtime_variable);
if (!entry) {
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) {
vm->error = 1;
return;
}
*df = (cell_t)(int64_t)
addr;
log_message(LOG_DEBUG, "VARIABLE: '%.*s' at VM addr %ld", nlen, namebuf, (long) addr);
}
/* [ ( -- ) enter interpret state — IMMEDIATE */
static void defining_word_left_bracket(VM *vm) {
if (!vm) return;
/* Forth-79 requires STATE to be 0 for interpret and non-zero (typically -1) for compile */
vm_store_cell(vm, vm->state_addr, 0);
vm->mode = MODE_INTERPRET;
log_message(LOG_DEBUG, "[: interpret mode");
}
/* ] ( -- ) enter compile state — IMMEDIATE */
static void defining_word_right_bracket(VM *vm) {
if (!vm) return;
vm_store_cell(vm, vm->state_addr, (cell_t) - 1);
vm->mode = MODE_COMPILE;
log_message(LOG_DEBUG, "]: compile mode");
}
/* STATE ( -- addr ) push VM addr of STATE cell */
static void defining_word_state(VM *vm) {
if (!vm) return;
vm_push(vm, (cell_t) vm->state_addr);
}
/* COMPILE ( "word" -- ) — IMMEDIATE (legacy) */
static void defining_word_compile(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
DictEntry *entry = vm_find_word(vm, namebuf, (size_t) nlen);
if (!entry) {
vm->error = 1;
return;
}
vm_compile_word(vm, entry);
log_message(LOG_DEBUG, "COMPILE: compiled '%.*s'", nlen, namebuf);
}
/* [COMPILE] ( "word" -- ) — IMMEDIATE: compile even if the word is IMMEDIATE */
static void defining_word_bracket_compile(VM *vm) {
defining_word_compile(vm);
}
/* LITERAL ( n -- ) — IMMEDIATE: compile a literal */
static void defining_word_literal(VM *vm) {
if (!vm) return;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t v = vm_pop(vm);
vm_compile_literal(vm, v);
log_message(LOG_DEBUG, "LITERAL: compiled %ld", (long) v);
}
/* FORGET ( "name" -- ) remove target and newer words; rewind HERE to target DFA */
static void dictionary_word_forget(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) {
vm->error = 1;
return;
}
int forget_failed = 0;
sf_mutex_lock(&vm->dict_lock);
/* Find target */
DictEntry *e = vm->latest;
DictEntry *target = NULL;
while (e) {
if (!(e->flags & WORD_HIDDEN) &&
e->name_len == (uint8_t) nlen &&
memcmp(e->name, namebuf, (size_t) nlen) == 0) {
target = e;
break;
}
e = e->link;
}
if (!target) {
vm->error = 1;
forget_failed = 1;
goto forget_exit;
}
/* Ensure target is NEWER than the fence */
int allowed = 0;
for (e = vm->latest; e; e = e->link) {
if (e == target) {
allowed = 1;
break;
}
if (e == vm->dict_fence_latest) break;
}
if (!allowed) {
vm->error = 1;
forget_failed = 1;
goto forget_exit;
}
/* Compute new HERE from target’s DFA (bounded by fence HERE) */
vaddr_t new_here = (vaddr_t) vm->here;
cell_t *df = vm_dictionary_get_data_field(target);
if (df) {
vaddr_t dfa = (vaddr_t)(uint64_t)(*df);
if (vm_addr_ok(vm, dfa, 0)) {
vaddr_t fence_here = (vaddr_t) vm->dict_fence_here;
new_here = (dfa < fence_here) ? fence_here : dfa;
}
} else {
new_here = (vaddr_t) vm->dict_fence_here;
}
/* Save target's link before freeing (needed for relinking) */
DictEntry *target_next = target->link;
/* Free headers from latest down to and including target, stopping before fence */
e = vm->latest;
while (e && e != vm->dict_fence_latest) {
DictEntry *next = e->link;
int is_target = (e == target);
vm_dictionary_untrack_entry(vm, e);
/* Free transition metrics before freeing entry */
if (e->transition_metrics) {
transition_metrics_cleanup(e->transition_metrics);
free(e->transition_metrics);
e->transition_metrics = NULL;
}
free(e);
if (is_target) break;
e = next;
}
/* Relink: FORGET removes target and everything newer (closer to
* vm->latest) -- the free loop above just freed every entry from
* vm->latest through target inclusive, which by definition includes
* any entry between them. There is nothing left to "skip past" --
* target_next (target's own surviving link) is always the correct
* new head, whether or not target was vm->latest itself. (Previously
* this branched on whether an entry existed between vm->latest and
* target, and relinked through it -- but that entry was always inside
* the just-freed range, making it a use-after-free on every FORGET of
* a non-latest word.) */
vm->latest = target_next;
vm->here = (size_t) new_here;
log_message(LOG_DEBUG, "FORGET: forgot '%.*s'; HERE=%ld (fence HERE=%ld)",
nlen, namebuf, (long) vm->here, (long) vm->dict_fence_here);
forget_exit:
sf_mutex_unlock(&vm->dict_lock);
if (forget_failed)
return;
}
/* FENCE ( -- ) raise the FORGET boundary to the current dictionary top.
* dict_fence_latest/dict_fence_here already exist and are already honored
* by FORGET (set once at bootstrap, protecting the base wordset); this is
* the first word exposing that state so callers can advance it further --
* e.g. an SDK capsule calling FENCE after loading protects its own
* definitions the same way the base wordset is already protected. */
static void dictionary_word_fence(VM *vm) {
if (!vm) return;
sf_mutex_lock(&vm->dict_lock);
vm->dict_fence_latest = vm->latest;
vm->dict_fence_here = vm->here;
sf_mutex_unlock(&vm->dict_lock);
log_message(LOG_DEBUG, "FENCE: raised to HERE=%ld", (long) vm->dict_fence_here);
}
/* DOES> — IMMEDIATE: finalize the defining word’s create-part and compile DOES>-body */
static void defining_word_does(VM *vm) {
if (!vm) return;
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
log_message(LOG_ERROR, "DOES>: compile-only");
return;
}
/* Compile helper (void fn; check vm->error after) */
vm_compile_call(vm, defining_runtime_does_rt);
if (vm->error) {
log_message(LOG_ERROR, "DOES>: could not compile does_rt");
return;
}
/* Compile EXIT so the defining word returns immediately after patching */
DictEntry *exitw = vm_find_word(vm, "EXIT", 4);
if (!exitw) {
vm->error = 1;
log_message(LOG_ERROR, "DOES>: EXIT not found");
return;
}
vm_compile_word(vm, exitw);
if (vm->error) return;
/* Continue compiling: the following words form the DOES>-body executed by DODOES later. */
}
/* IMMEDIATE ( -- ) mark latest word immediate — IMMEDIATE itself */
static void defining_word_immediate(VM *vm) {
if (!vm) return;
if (!vm->latest) {
vm->error = 1;
return;
}
vm->latest->flags |= WORD_IMMEDIATE;
log_message(LOG_DEBUG, "IMMEDIATE: marked '%.*s' immediate",
(int) vm->latest->name_len, vm->latest->name);
}
/* ───────────────────────────── DEFER / IS ─────────────────────────── */
/* DEFER/IS/DEFER@ are NOT defined here: defer_words.c registers the same
* three names later in boot order (word_registry.c Module 27 vs this
* file's Module 17), and it has no __STARKERNEL__ guard, so it shadows
* this file's versions in both hosted and kernel builds. FORTH's
* newest-first dictionary lookup means defer_words.c's versions (a
* self-contained implementation, unrelated to this file's former
* defining_runtime_defer) are the only ones ever reachable. See
* proof/StarForth_Defer_Words.thy and proof/FINDINGS.md §3. */
/* ───────────────────────────── Registration ───────────────────────── */
/**
* @brief Register all defining words in the dictionary
*
* Registers core defining words like :, ;, CREATE, CONSTANT, VARIABLE,
* and their support words like IMMEDIATE, STATE, etc.
*
* @param vm Pointer to the VM context
*/
void register_defining_words(VM *vm) {
/* Core colon pair — both IMMEDIATE */
register_word(vm, ":", defining_word_colon);
vm_make_immediate(vm);
register_word(vm, ";", defining_word_semicolon);
vm_make_immediate(vm);
/* CREATE / VARIABLE / CONSTANT */
register_word(vm, "CREATE", defining_word_create);
register_word(vm, "VARIABLE", defining_word_variable);
register_word(vm, "CONSTANT", defining_word_constant);
/* IMMEDIATE (make IMMEDIATE itself immediate) */
register_word(vm, "IMMEDIATE", defining_word_immediate);
vm_make_immediate(vm);
/* STATE and mode switchers */
register_word(vm, "STATE", defining_word_state);
register_word(vm, "[", defining_word_left_bracket);
vm_make_immediate(vm);
register_word(vm, "]", defining_word_right_bracket);
vm_make_immediate(vm);
/* Dictionary management */
register_word(vm, "FORGET", dictionary_word_forget);
register_word(vm, "FENCE", dictionary_word_fence);
/* Compile helpers — immediate */
register_word(vm, "COMPILE", defining_word_compile);
vm_make_immediate(vm);
register_word(vm, "[COMPILE]", defining_word_bracket_compile);
vm_make_immediate(vm);
/* LIT runtime + LITERAL (immediate) */
register_word(vm, "LIT", defining_runtime_lit);
register_word(vm, "LITERAL", defining_word_literal);
vm_make_immediate(vm);
/* DOES> plumbing (lowercase internal helper visible for tests/trace) */
register_word(vm, "does_rt", defining_runtime_does_rt); /* internal helper */
register_word(vm, "DOES>", defining_word_does);
vm_make_immediate(vm);
}
@@ -0,0 +1,287 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
*** StarForth ***
dictionary_heat_diagnostic_words.c - FORTH Interface for Dictionary Heat Optimization
Introspection words to observe and control heat-aware dictionary lookup behavior.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "vm.h"
#include "word_registry.h"
#include "platform_time.h"
#include "dictionary_heat_optimization.h"
/**
* @brief HEAT-PERCENTILES ( -- 25th 50th 75th )
*
* Pushes the three heat percentile thresholds currently used by the
* heat-aware dictionary lookup strategy: @c heat_threshold_25th,
* @c heat_threshold_50th, and @c heat_threshold_75th. These are updated
* by @c dict_update_heat_percentiles() during bucket reorganisation.
*
* Stack effect: ( -- 25th 50th 75th ) TOS = 75th
*
* @param vm Active VM; sets @c vm->error = 1 on stack overflow
*/
void forth_HEAT_PERCENTILES(VM *vm) {
if (vm->error) return;
if (vm->dsp + 3 > STACK_SIZE) {
vm->error = 1;
return;
}
vm->data_stack[vm->dsp++] = vm->heat_threshold_25th;
vm->data_stack[vm->dsp++] = vm->heat_threshold_50th;
vm->data_stack[vm->dsp++] = vm->heat_threshold_75th;
}
/**
* @brief LOOKUP-STRATEGY@ ( -- strategy )
*
* Pushes the current dictionary lookup strategy: 0 = naive (newest-first
* linear scan), 1 = heat-aware (hot-bucket-first).
*
* Stack effect: ( -- strategy )
*
* @param vm Active VM; sets @c vm->error = 1 on stack overflow
*/
void forth_LOOKUP_STRATEGY_FETCH(VM *vm) {
if (vm->error) return;
if (vm->dsp >= STACK_SIZE) {
vm->error = 1;
return;
}
vm->data_stack[vm->dsp++] = (cell_t)vm->lookup_strategy;
}
/**
* @brief LOOKUP-STRATEGY! ( strategy -- )
*
* Sets @c vm->lookup_strategy to @c strategy. Only values 0 (naive) and
* 1 (heat-aware) are accepted; out-of-range values are silently discarded.
*
* Stack effect: ( strategy -- )
*
* @param vm Active VM; sets @c vm->error = 1 on stack underflow
*/
void forth_LOOKUP_STRATEGY_STORE(VM *vm) {
if (vm->error) return;
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t strategy = vm->data_stack[--vm->dsp];
if (strategy == 0 || strategy == 1) {
vm->lookup_strategy = (int)strategy;
}
}
/**
* @brief REORG-BUCKETS ( -- )
*
* Immediately triggers @c dict_reorganize_buckets_by_heat() followed by
* @c dict_update_heat_percentiles() to re-sort the dictionary's lookup
* buckets by current execution heat and refresh the percentile thresholds.
* Normally this happens automatically on a heartbeat cycle; this word forces
* it on-demand for testing or after a large batch of @c HEAT! assignments.
*
* Stack effect: ( -- )
*
* @param vm Active VM; no-op if @c vm->error is set
*/
void forth_REORG_BUCKETS(VM *vm) {
if (vm->error) return;
dict_reorganize_buckets_by_heat(vm);
dict_update_heat_percentiles(vm);
}
/**
* @brief SHOW-HEAT-OPTIMIZATION ( -- )
*
* Prints a summary of the current dictionary heat optimization state to
* @c stdout: the active lookup strategy (naive or heat-aware), the three
* percentile threshold values, and the resulting hot/warm/cool heat zones.
* Intended for interactive diagnostics.
*
* Stack effect: ( -- )
*
* @param vm Active VM; no-op if @c vm->error is set
*/
void forth_SHOW_HEAT_OPTIMIZATION(VM *vm) {
if (vm->error) return;
printf("\n=== Dictionary Heat Optimization Status ===\n");
printf("Lookup Strategy: ");
if (vm->lookup_strategy == 0) {
printf("NAIVE (newest-first search)\n");
} else if (vm->lookup_strategy == 1) {
printf("HEAT-AWARE (hot words first)\n");
} else {
printf("UNKNOWN (%d)\n", vm->lookup_strategy);
}
printf("Heat Percentile Thresholds:\n");
printf(" 25th percentile: %ld\n", vm->heat_threshold_25th);
printf(" 50th percentile: %ld\n", vm->heat_threshold_50th);
printf(" 75th percentile: %ld\n", vm->heat_threshold_75th);
printf("\nHeat Zones:\n");
printf(" TOP 25%% (hot) : >= %ld heat\n", vm->heat_threshold_75th);
printf(" MIDDLE 50%% (warm) : %ld - %ld heat\n",
vm->heat_threshold_25th, vm->heat_threshold_75th);
printf(" BOTTOM 25%% (cool) : < %ld heat\n", vm->heat_threshold_25th);
printf("\n");
}
/**
* @brief COMPARE-LOOKUPS ( iterations -- )
*
* Micro-benchmark that measures naive vs heat-aware dictionary lookup
* performance over @c iterations passes through six common test words
* (DUP, DROP, SWAP, @, !, EMIT). Prints elapsed time in milliseconds and
* the percentage speedup (or slowdown) for heat-aware vs naive. Restores
* the original @c vm->lookup_strategy on completion. Intended for
* interactive diagnostics; not used in DoE mode.
*
* Stack effect: ( iterations -- )
*
* @param vm Active VM; sets @c vm->error = 1 on underflow or non-positive @c iterations
*/
void forth_COMPARE_LOOKUPS(VM *vm) {
if (vm->error) return;
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t iterations = vm->data_stack[--vm->dsp];
if (iterations <= 0) {
printf("Need positive iteration count\n");
return;
}
printf("\n=== Dictionary Lookup Comparison ===\n");
printf("Iterations: %ld\n", iterations);
/* Common test words */
const char *test_words[] = {"DUP", "DROP", "SWAP", "@", "!", "EMIT", NULL};
/* Strategy 1: Naive (current strategy) */
printf("\nNaive (newest-first) lookup:\n");
int orig_strategy = vm->lookup_strategy;
vm->lookup_strategy = 0;
uint64_t start_ns = sf_monotonic_ns();
for (cell_t i = 0; i < iterations; i++) {
for (int j = 0; test_words[j]; j++) {
vm_find_word(vm, test_words[j], strlen(test_words[j]));
}
}
uint64_t naive_ns = sf_monotonic_ns() - start_ns;
/* Strategy 2: Heat-aware */
printf("Heat-aware lookup:\n");
dict_reorganize_buckets_by_heat(vm);
vm->lookup_strategy = 1;
start_ns = sf_monotonic_ns();
for (cell_t i = 0; i < iterations; i++) {
for (int j = 0; test_words[j]; j++) {
vm_find_word(vm, test_words[j], strlen(test_words[j]));
}
}
uint64_t heat_ns = sf_monotonic_ns() - start_ns;
/* Restore original strategy */
vm->lookup_strategy = orig_strategy;
/* Report results_run_01_2025_12_08 */
printf("\n=== Benchmark Results ===\n");
printf("Naive time: %.3f ms\n", (double)naive_ns / 1000000.0);
printf("Heat-aware time: %.3f ms\n", (double)heat_ns / 1000000.0);
if (heat_ns > 0) {
double speedup = (double)naive_ns / (double)heat_ns;
if (speedup > 1.0) {
printf("Speedup: %.2f%% faster\n", (speedup - 1.0) * 100.0);
} else {
printf("Slowdown: %.2f%% slower\n", (1.0 - speedup) * 100.0);
}
}
printf("\n");
}
/**
* @brief Register all dictionary heat diagnostic words with the VM dictionary.
*
* Registers: @c HEAT-PERCENTILES, @c LOOKUP-STRATEGY@, @c LOOKUP-STRATEGY!,
* @c REORG-BUCKETS, @c SHOW-HEAT-OPTIMIZATION, @c COMPARE-LOOKUPS.
* Called during VM bootstrap.
*
* @param vm Active VM to register words into
*/
void register_dictionary_heat_diagnostic_words(VM *vm) {
register_word(vm, "HEAT-PERCENTILES", forth_HEAT_PERCENTILES);
register_word(vm, "LOOKUP-STRATEGY@", forth_LOOKUP_STRATEGY_FETCH);
register_word(vm, "LOOKUP-STRATEGY!", forth_LOOKUP_STRATEGY_STORE);
register_word(vm, "REORG-BUCKETS", forth_REORG_BUCKETS);
register_word(vm, "SHOW-HEAT-OPTIMIZATION", forth_SHOW_HEAT_OPTIMIZATION);
register_word(vm, "COMPARE-LOOKUPS", forth_COMPARE_LOOKUPS);
}
@@ -0,0 +1,469 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* dictionary_manipulation_words.c - FORTH-79 Dictionary Manipulation Words */
#include "include/dictionary_manipulation_words.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
/* Global state for dictionary manipulation */
static cell_t state_variable = 0; /* STATE - compilation state */
/* Helper function to calculate aligned address */
static uintptr_t align_address(uintptr_t addr) {
size_t alignment = sizeof(cell_t);
return (addr + alignment - 1) & ~(alignment - 1);
}
/* Helper function to get name field from execution token */
static char *get_name_field(DictEntry *entry) {
if (entry == NULL) {
return NULL;
}
return (char *) entry->name;
}
/* Helper function to get body address from execution token */
static void *get_body_address(DictEntry *entry) {
if (entry == NULL) {
return NULL;
}
/* Body follows the header structure */
uintptr_t body_addr = (uintptr_t) entry + sizeof(DictEntry) + entry->name_len;
return (void *) align_address(body_addr);
}
/* Helper function to find dictionary entry from name field */
static DictEntry *find_entry_from_name(VM *vm, char *name_field) {
DictEntry *entry;
entry = vm->latest;
while (entry != NULL) {
if ((char *) entry->name == name_field) {
return entry;
}
entry = entry->link;
}
return NULL;
}
/* Helper function to traverse name field */
static char *traverse_name_field(char *name_addr, int direction) {
uint8_t *ptr;
uint8_t name_len;
if (name_addr == NULL) {
return NULL;
}
/* FORTH-79 names start with length byte */
ptr = (uint8_t *) name_addr;
if (direction > 0) {
/* Forward traversal - skip over name */
name_len = *ptr & 0x1F; /* Mask off flags */
return (char *) (ptr + name_len + 1);
} else {
/* Backward traversal - find start of name */
/* This is more complex - we need to scan backwards */
/* For simplicity, assume we're at the start already */
return name_addr;
}
}
/**
* @brief FORTH word SMUDGE - Toggle smudge bit of latest word
* @param vm Pointer to VM instance
* @stack ( -- )
* @note Compile-only word
*/
static void dictionary_m_word_smudge(VM *vm) {
// compile-only: error if used while interpreting
if (vm->mode != MODE_COMPILE) {
// correct
vm->error = 1;
return;
}
// existing body that toggles/sets the smudge/hidden bit on the *latest* word
// (leave your current logic exactly as-is)
}
/* >BODY ( xt -- addr ) Convert execution token to body */
void dictionary_m_word_to_body(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, ">BODY: Stack underflow");
vm->error = 1;
return;
}
cell_t xt = vm_pop(vm);
log_message(LOG_DEBUG, ">BODY: POP: %ld (dsp=%d)", (long) xt, vm->dsp);
DictEntry *entry = (DictEntry *) (intptr_t) xt;
if (!entry) {
log_message(LOG_ERROR, ">BODY: Invalid execution token (NULL)");
vm->error = 1;
return;
}
// Use the VM's dictionary function to get the data field
cell_t *data_field = vm_dictionary_get_data_field(entry);
if (!data_field) {
log_message(LOG_ERROR, ">BODY: Unable to get data field address for entry %p", (void *) entry);
vm->error = 1;
return;
}
cell_t body_addr = (cell_t)(intptr_t)data_field;
log_message(LOG_DEBUG, ">BODY: xt=%p -> body=%p", (void *) entry, (void *) data_field);
vm_push(vm, body_addr);
log_message(LOG_DEBUG, ">BODY: PUSH: %ld (dsp=%d)", (long) body_addr, vm->dsp);
}
/* >NAME ( xt -- addr ) Convert execution token to name */
void dictionary_m_word_to_name(VM *vm) {
cell_t xt;
DictEntry *entry;
char *name_field;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
xt = vm_pop(vm);
entry = (DictEntry *) (uintptr_t) xt;
name_field = get_name_field(entry);
if (name_field == NULL) {
vm->error = 1;
return;
}
vm_push(vm, (cell_t)(uintptr_t)name_field);
}
/* NAME> ( addr -- xt ) Convert name to execution token */
void dictionary_m_word_name_to(VM *vm) {
cell_t addr;
char *name_field;
DictEntry *entry;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
addr = vm_pop(vm);
name_field = (char *) (uintptr_t) addr;
entry = find_entry_from_name(vm, name_field);
if (entry == NULL) {
vm->error = 1;
return;
}
vm_push(vm, (cell_t)(uintptr_t)entry);
}
/* >LINK ( addr -- addr ) Get link field address */
void dictionary_m_word_to_link(VM *vm) {
cell_t addr;
DictEntry *entry;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
addr = vm_pop(vm);
entry = (DictEntry *) (uintptr_t) addr;
if (entry == NULL) {
vm->error = 1;
return;
}
/* Link field is part of the DictEntry structure */
vm_push(vm, (cell_t)(uintptr_t) & entry->link);
}
/* LINK> ( addr -- addr ) Get next word from link */
void dictionary_m_word_link_from(VM *vm) {
cell_t addr;
DictEntry **link_field;
DictEntry *next_entry;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
addr = vm_pop(vm);
link_field = (DictEntry **) (uintptr_t) addr;
if (link_field == NULL) {
vm->error = 1;
return;
}
next_entry = *link_field;
vm_push(vm, (cell_t)(uintptr_t)next_entry);
}
/* CFA ( addr -- xt ) Get code field address */
void dictionary_m_word_cfa(VM *vm) {
cell_t addr;
DictEntry *entry;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
addr = vm_pop(vm);
entry = (DictEntry *) (uintptr_t) addr;
if (entry == NULL) {
vm->error = 1;
return;
}
/* In our implementation, the execution token is the entry itself */
vm_push(vm, (cell_t)(uintptr_t)entry);
}
/* LFA ( addr -- addr ) Get link field address */
void dictionary_m_word_lfa(VM *vm) {
dictionary_m_word_to_link(vm); /* Same as >LINK */
}
/* NFA ( addr -- addr ) Get name field address */
void dictionary_m_word_nfa(VM *vm) {
cell_t addr;
DictEntry *entry;
char *name_field;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
addr = vm_pop(vm);
entry = (DictEntry *) (uintptr_t) addr;
name_field = get_name_field(entry);
if (name_field == NULL) {
vm->error = 1;
return;
}
vm_push(vm, (cell_t)(uintptr_t)name_field);
}
/* PFA ( addr -- addr ) Get parameter field address */
void dictionary_m_word_pfa(VM *vm) {
cell_t addr;
DictEntry *entry;
void *body_addr;
if (vm->dsp < 0) {
vm->error = 1;
return;
}
addr = vm_pop(vm);
entry = (DictEntry *) (uintptr_t) addr;
body_addr = get_body_address(entry);
if (body_addr == NULL) {
vm->error = 1;
return;
}
vm_push(vm, (cell_t)(uintptr_t)body_addr);
}
/* TRAVERSE ( addr n -- addr ) Move through name field */
void dictionary_m_word_traverse(VM *vm) {
cell_t n, addr;
char *name_addr;
char *result_addr;
if (vm->dsp < 1) {
vm->error = 1;
return;
}
n = vm_pop(vm);
addr = vm_pop(vm);
name_addr = (char *) (uintptr_t) addr;
result_addr = traverse_name_field(name_addr, (int) n);
if (result_addr == NULL) {
vm->error = 1;
return;
}
vm_push(vm, (cell_t)(uintptr_t)result_addr);
}
/* INTERPRET ( -- ) Text interpreter */
void dictionary_m_word_interpret(VM *vm) {
/* Simply call the VM's built-in interpreter - it handles everything */
/* The VM already has proper input management, word parsing, and execution */
/* In FORTH-79, INTERPRET processes the current input stream */
/* Since vm_interpret handles input parsing and execution, we just ensure */
/* we're in the right mode and let the VM do the work */
/* Set interpretation mode */
vm->mode = MODE_INTERPRET;
state_variable = 0;
/* The actual interpretation happens through vm_interpret() calls */
/* This word exists mainly for completeness and mode setting */
}
static void dictionary_m_word_find(VM *vm) {
char namebuf[128];
int nlen = vm_parse_word(vm, namebuf, sizeof namebuf);
if (nlen <= 0) {
vm->error = 1; // real input underflow
return;
}
DictEntry *e = vm_find_word(vm, namebuf, (size_t) nlen);
if (e) {
vm_push(vm, (cell_t)(uintptr_t)e); // compilation address; swap to CFA if you prefer later
} else {
vm_push(vm, 0); // miss is NOT an error
}
}
/* ' ( -- xt ) FORTH-79 tick: parse next word, push its execution token.
* Non-immediate — in compile mode the interpreter compiles a call to ',
* so this function only ever runs in interpret mode. */
static void dictionary_m_word_tick(VM *vm) {
char namebuf[128];
int nlen = vm_parse_word(vm, namebuf, sizeof namebuf);
if (nlen <= 0) {
log_message(LOG_ERROR, "': unable to parse word");
vm->error = 1;
return;
}
DictEntry *e = vm_find_word(vm, namebuf, (size_t) nlen);
if (!e) {
log_message(LOG_ERROR, "': word '%.*s' not found", nlen, namebuf);
vm->error = 1;
return;
}
log_message(LOG_DEBUG, "': found '%.*s' xt=%p", nlen, namebuf, (void *) e);
vm_push(vm, (cell_t)(uintptr_t)e);
}
// in src/word_source/dictionary_manipulation_words.c
static void dictionary_m_word_hidden(VM *vm) {
// compile-only guard (matches your other compile-only words)
if (vm->mode != MODE_COMPILE) {
vm->error = 1;
return;
}
// get the latest entry (use whatever you use elsewhere; many places access vm->latest)
DictEntry *e = vm->latest;
if (!e) {
vm->error = 1;
return;
}
// Set the hidden/smudge flag. Prefer a named flag if you have it.
// If your codebase uses WORD_HIDDEN, use that.
// If it uses the same bit as SMUDGE, use that (often called WORD_SMUDGED).
#ifdef WORD_HIDDEN
e->flags |= WORD_HIDDEN;
#else
// Fallback: if you only have a toggling SMUDGE path, set conditionally using it.
if (!(e->flags & WORD_SMUDGED)) {
// call your existing SMUDGE implementation to flip it on once
dictionary_word_smudge(vm);
// ensure we didn't accidentally set an error
if (vm->error) return;
}
#endif
}
/*
* @brief Register all dictionary manipulation words with the VM
* @param vm Pointer to VM instance
* @details Registers standard FORTH-79 dictionary manipulation words
*/
void register_dictionary_manipulation_words(VM *vm) {
/* Register all dictionary manipulation words */
/* [, ], STATE are NOT registered here: defining_words.c registers the
* same three names later in boot order (word_registry.c Module 17 vs
* this file's Module 13), and FORTH's newest-first dictionary lookup
* means defining_words.c's versions are the only ones ever reachable.
* The versions formerly here also wrote a dead file-scope static
* instead of vm->state_addr -- see proof/StarForth_Defining_Words.thy
* and proof/FINDINGS.md §1. */
register_word(vm, "SMUDGE", dictionary_m_word_smudge);
register_word(vm, "HIDDEN", dictionary_m_word_hidden);
register_word(vm, ">BODY", dictionary_m_word_to_body);
register_word(vm, ">NAME", dictionary_m_word_to_name);
register_word(vm, "NAME>", dictionary_m_word_name_to);
register_word(vm, ">LINK", dictionary_m_word_to_link);
register_word(vm, "LINK>", dictionary_m_word_link_from);
register_word(vm, "CFA", dictionary_m_word_cfa);
register_word(vm, "LFA", dictionary_m_word_lfa);
register_word(vm, "NFA", dictionary_m_word_nfa);
register_word(vm, "PFA", dictionary_m_word_pfa);
register_word(vm, "TRAVERSE", dictionary_m_word_traverse);
register_word(vm, "INTERPRET", dictionary_m_word_interpret);
register_word(vm, "FIND", dictionary_m_word_find);
register_word(vm, "'", dictionary_m_word_tick);
}
+224
View File
@@ -0,0 +1,224 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "include/dictionary_words.h"
#include "../../include/word_registry.h"
#include "vm.h"
#include "../../include/log.h"
/* === SAFE DICTIONARY WORDS IMPLEMENTATION (addresses are VM offsets) === */
/**
* @brief HERE ( -- addr ) Returns the dictionary pointer
* @param vm Pointer to the VM structure
*/
void dictionary_word_here(VM *vm) {
vm_push(vm, vm->here);
}
static void dictionary_word_align(VM *vm) {
vm_align(vm);
}
/**
* @brief ALLOT ( n -- ) Allocate n bytes in dictionary
* @param vm Pointer to the VM structure
* @details Allocates n bytes in the dictionary. n can be negative to deallocate space
*/
void dictionary_word_allot(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
cell_t new_here = vm->here + n;
/* Dictionary growth is bounded to DICTIONARY_MEMORY_SIZE, not the full
* VM_MEMORY_SIZE: blocks >= USER_BLOCKS_START live in the same flat
* vm->memory array (see vm_get_block_addr), so letting `here` grow past
* the dictionary region would silently corrupt live block/user data. */
if (new_here < 0 || new_here > (cell_t) DICTIONARY_MEMORY_SIZE) {
vm->error = 1;
return;
}
vm->here = new_here;
}
/**
* @brief , ( n -- ) Compile cell into dictionary
* @param vm Pointer to the VM structure
* @details Compiles a single cell value into the dictionary at HERE
*/
void dictionary_word_comma(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
vm_align(vm);
if (vm->here + (cell_t) sizeof(cell_t) > DICTIONARY_MEMORY_SIZE) {
vm->error = 1;
return;
}
vaddr_t addr = VM_ADDR(vm->here);
if (!vm_addr_ok(vm, addr, sizeof(cell_t))) {
vm->error = 1;
return;
}
vm_store_cell(vm, addr, n);
vm->here += sizeof(cell_t);
}
/**
* @brief C, ( c -- ) Compile byte into dictionary
* @param vm Pointer to the VM structure
* @details Compiles a single byte value into the dictionary at HERE
*/
void dictionary_word_c_comma(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t c = vm_pop(vm);
if (vm->here + 1 > DICTIONARY_MEMORY_SIZE) {
vm->error = 1;
return;
}
vaddr_t a = VM_ADDR(vm->here);
if (!vm_addr_ok(vm, a, 1)) {
vm->error = 1;
return;
}
vm_store_u8(vm, a, (uint8_t)(c & 0xFF));
vm->here += 1;
}
/**
* @brief 2, ( d -- ) Compile double-cell into dictionary
* @param vm Pointer to the VM structure
* @details Compiles a double cell value into the dictionary at HERE, low cell first
*/
void dictionary_word_2comma(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t high = vm_pop(vm);
cell_t low = vm_pop(vm);
vm_align(vm);
if (vm->here + (cell_t)(2 * sizeof(cell_t)) > DICTIONARY_MEMORY_SIZE) {
vm->error = 1;
return;
}
vaddr_t a = VM_ADDR(vm->here);
if (!vm_addr_ok(vm, a, 2 * sizeof(cell_t))) {
vm->error = 1;
return;
}
vm_store_cell(vm, a, low);
vm_store_cell(vm, a + sizeof(cell_t), high);
vm->here += 2 * sizeof(cell_t);
}
/**
* @brief PAD ( -- addr ) Get scratch pad buffer address
* @param vm Pointer to the VM structure
* @details Returns VM address of 512-byte temporary text buffer at top of memory
*/
void dictionary_word_pad(VM *vm) {
size_t pad_offset = VM_MEMORY_SIZE - 512;
vm_push(vm, (cell_t) pad_offset);
}
/**
* @brief SP@ ( -- sp ) Get current stack pointer
* @param vm Pointer to the VM structure
* @details Returns the current stack-pointer index (top is 0)
*/
void dictionary_word_sp_fetch(VM *vm) {
/* No stack args required */
vm_push(vm, (cell_t) vm->dsp);
}
/**
* @brief SP! ( sp -- ) Set stack pointer
* @param vm Pointer to the VM structure
* @details Restores the stack-pointer, but never allows growing the stack
*/
void dictionary_word_sp_store(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
} /* need one arg */
cell_t new_sp = vm_pop(vm);
/* Valid range: -1 (empty stack) up to current dsp (can shrink, not grow) */
if (new_sp < -1 || new_sp > vm->dsp) {
vm->error = 1;
return;
}
vm->dsp = new_sp;
/* We don’t need to scrub values above dsp; they’re considered garbage/unused. */
}
/**
* @brief LATEST ( -- addr ) Get latest definition address
* @param vm Pointer to the VM structure
* @details Returns VM address near most recent compiled definition (end of dictionary)
*/
void dictionary_word_latest(VM *vm) {
vm_push(vm, vm->here);
}
/* Registration */
void register_dictionary_words(VM *vm) {
register_word(vm, "HERE", dictionary_word_here);
register_word(vm, "ALIGN", dictionary_word_align);
register_word(vm, "ALLOT", dictionary_word_allot);
register_word(vm, ",", dictionary_word_comma);
register_word(vm, "C,", dictionary_word_c_comma);
register_word(vm, "2,", dictionary_word_2comma);
register_word(vm, "PAD", dictionary_word_pad);
register_word(vm, "SP!", dictionary_word_sp_store);
register_word(vm, "SP@", dictionary_word_sp_fetch);
register_word(vm, "LATEST", dictionary_word_latest);
}
+485
View File
@@ -0,0 +1,485 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* double_words.c - FORTH-79 Double Number Words */
#include "include/double_words.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
/**
* @brief Convert single number to double ( n -- d )
*
* Converts a single-precision number to double-precision format.
* Result is pushed as ( dlow dhigh ) with high cell on top.
*
* @param vm Pointer to the virtual machine state
*/
void double_word_s_to_d(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
cell_t dhigh = (n < 0) ? -1 : 0;
vm_push(vm, n); // push low
vm_push(vm, dhigh); // push high
}
/* D+ ( d1 d2 -- d3 ) */
void double_word_d_plus(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
// Stack layout: d1low d1high d2low d2high (top)
// Pop from top: d2high, d2low, d1high, d1low
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
// Use unsigned arithmetic for carry detection. ucell_t (vm.h), not plain
// unsigned long -- unsigned long is only 32-bit on some bare-metal
// aarch64 cross-compile targets, silently truncating this addition and
// losing the sign-extension D. depends on (FABRIC-3.md §XII.4).
ucell_t ud1low = (ucell_t) d1low;
ucell_t ud2low = (ucell_t) d2low;
ucell_t uresult_low = ud1low + ud2low;
cell_t result_low = (cell_t) uresult_low;
cell_t carry = (uresult_low < ud1low) ? 1 : 0;
cell_t result_high = d1high + d2high + carry;
// Push result: low first, then high (so high is on top)
vm_push(vm, result_low);
vm_push(vm, result_high);
}
/* D- ( d1 d2 -- d3 ) */
void double_word_d_minus(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
// Stack layout: d1low d1high d2low d2high (top)
// Pop from top: d2high, d2low, d1high, d1low
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
// Use unsigned arithmetic for borrow detection. ucell_t, not plain
// unsigned long -- see double_word_d_plus()'s matching comment.
ucell_t ud1low = (ucell_t) d1low;
ucell_t ud2low = (ucell_t) d2low;
ucell_t uresult_low = ud1low - ud2low;
cell_t result_low = (cell_t) uresult_low;
cell_t borrow = (ud1low < ud2low) ? 1 : 0;
cell_t result_high = d1high - d2high - borrow;
// Push result: low first, then high (so high is on top)
vm_push(vm, result_low);
vm_push(vm, result_high);
}
/* DNEGATE ( d1 -- d2 ) */
void double_word_dnegate(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
// Stack layout: dlow dhigh (top)
cell_t dhigh = vm_pop(vm);
cell_t dlow = vm_pop(vm);
// Negate: ~d + 1. ucell_t, not plain unsigned long -- see
// double_word_d_plus()'s matching comment.
ucell_t new_low = ~(ucell_t) dlow + 1;
cell_t new_dhigh = ~dhigh + (new_low == 0 ? 1 : 0);
vm_push(vm, (cell_t) new_low);
vm_push(vm, new_dhigh);
}
/* DABS ( d1 -- d2 ) */
void double_word_dabs(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
// High cell is on top of stack (dsp points to top)
cell_t dhigh = vm->data_stack[vm->dsp];
if (dhigh < 0) {
double_word_dnegate(vm);
}
}
/**
* @brief Compare two double numbers
*
* @param d1h High cell of first double number
* @param d1l Low cell of first double number
* @param d2h High cell of second double number
* @param d2l Low cell of second double number
* @return int -1 if d1<d2, 0 if d1=d2, 1 if d1>d2
*/
static int d_compare(cell_t d1h, cell_t d1l, cell_t d2h, cell_t d2l) {
if (d1h < d2h) return -1;
if (d1h > d2h) return 1;
// ucell_t, not plain unsigned long -- see double_word_d_plus()'s
// matching comment (unsigned long is only 32-bit on some bare-metal
// aarch64 cross-compile targets).
ucell_t ul1 = (ucell_t) d1l;
ucell_t ul2 = (ucell_t) d2l;
return (ul1 > ul2) - (ul1 < ul2);
}
/* DMAX ( d1 d2 -- d3 ) */
void double_word_dmax(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
if (d_compare(d1high, d1low, d2high, d2low) >= 0) {
vm_push(vm, d1low);
vm_push(vm, d1high);
} else {
vm_push(vm, d2low);
vm_push(vm, d2high);
}
}
/* DMIN ( d1 d2 -- d3 ) */
void double_word_dmin(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
if (d_compare(d1high, d1low, d2high, d2low) <= 0) {
vm_push(vm, d1low);
vm_push(vm, d1high);
} else {
vm_push(vm, d2low);
vm_push(vm, d2high);
}
}
/* D< ( d1 d2 -- flag ) */
void double_word_d_less(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
int cmp = d_compare(d1high, d1low, d2high, d2low);
vm_push(vm, (cmp < 0) ? -1 : 0);
}
/* D= ( d1 d2 -- flag ) */
void double_word_d_equals(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
int cmp = d_compare(d1high, d1low, d2high, d2low);
vm_push(vm, (cmp == 0) ? -1 : 0);
}
/* 2DROP ( d -- ) */
void double_word_2drop(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
vm_pop(vm);
vm_pop(vm);
}
/* 2DUP ( d -- d d ) */
void double_word_2dup(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
// Stack: dlow dhigh (top)
cell_t dhigh = vm->data_stack[vm->dsp];
cell_t dlow = vm->data_stack[vm->dsp - 1];
vm_push(vm, dlow);
vm_push(vm, dhigh);
}
/* 2SWAP ( d1 d2 -- d2 d1 ) */
void double_word_2swap(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
vm_push(vm, d2low);
vm_push(vm, d2high);
vm_push(vm, d1low);
vm_push(vm, d1high);
}
/* 2OVER ( d1 d2 -- d1 d2 d1 ) */
void double_word_2over(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
return;
}
// Stack: d1low d1high d2low d2high (top)
// Indices: dsp-3 dsp-2 dsp-1 dsp
cell_t d1low = vm->data_stack[vm->dsp - 3];
cell_t d1high = vm->data_stack[vm->dsp - 2];
vm_push(vm, d1low);
vm_push(vm, d1high);
}
/* 2ROT ( d1 d2 d3 -- d2 d3 d1 ) */
void double_word_2rot(VM *vm) {
if (vm->dsp < 5) {
vm->error = 1;
return;
}
cell_t d3high = vm_pop(vm);
cell_t d3low = vm_pop(vm);
cell_t d2high = vm_pop(vm);
cell_t d2low = vm_pop(vm);
cell_t d1high = vm_pop(vm);
cell_t d1low = vm_pop(vm);
vm_push(vm, d2low);
vm_push(vm, d2high);
vm_push(vm, d3low);
vm_push(vm, d3high);
vm_push(vm, d1low);
vm_push(vm, d1high);
}
/* 2>R ( d -- ) ( R: -- d ) */
void double_word_2to_r(VM *vm) {
if (vm->dsp < 1) { vm->error = 1; return; }
cell_t dhigh = vm_pop(vm);
cell_t dlow = vm_pop(vm);
if (vm->ecw_nesting > 0) {
/* Inside execute_colon_word: RS[rsp]=resume_ip; insert dlow,dhigh below it. */
if (vm->rsp + 2 >= STACK_SIZE) { vm->error = 1; return; }
cell_t resume_ip = vm->return_stack[vm->rsp];
vm->return_stack[vm->rsp] = dlow;
vm->return_stack[vm->rsp + 1] = dhigh;
vm->return_stack[vm->rsp + 2] = resume_ip;
vm->rsp += 2;
} else {
if (vm->rsp + 2 >= STACK_SIZE) { vm->error = 1; return; }
vm->return_stack[++vm->rsp] = dlow;
vm->return_stack[++vm->rsp] = dhigh;
}
}
/* 2R> ( -- d ) ( R: d -- ) */
void double_word_2r_from(VM *vm) {
if (vm->ecw_nesting > 0) {
/* Inside execute_colon_word: RS[rsp]=resume_ip, [rsp-1]=dhigh, [rsp-2]=dlow. */
if (vm->rsp < 2) { vm->error = 1; return; }
cell_t resume_ip = vm->return_stack[vm->rsp];
cell_t dhigh = vm->return_stack[vm->rsp - 1];
cell_t dlow = vm->return_stack[vm->rsp - 2];
vm->return_stack[vm->rsp - 2] = resume_ip;
vm->rsp -= 2;
vm_push(vm, dlow);
vm_push(vm, dhigh);
} else {
if (vm->rsp < 1) { vm->error = 1; return; }
cell_t dhigh = vm->return_stack[vm->rsp--];
cell_t dlow = vm->return_stack[vm->rsp--];
vm_push(vm, dlow);
vm_push(vm, dhigh);
}
}
/* 2R@ ( -- d ) ( R: d -- d ) */
void double_word_2r_fetch(VM *vm) {
if (vm->ecw_nesting > 0) {
/* Inside execute_colon_word: RS[rsp]=resume_ip, [rsp-1]=dhigh, [rsp-2]=dlow. */
if (vm->rsp < 2) { vm->error = 1; return; }
vm_push(vm, vm->return_stack[vm->rsp - 2]);
vm_push(vm, vm->return_stack[vm->rsp - 1]);
} else {
if (vm->rsp < 1) { vm->error = 1; return; }
vm_push(vm, vm->return_stack[vm->rsp - 1]);
vm_push(vm, vm->return_stack[vm->rsp]);
}
}
/* D0= ( d -- flag ) */
void double_word_d_zero_equals(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t dhigh = vm_pop(vm);
cell_t dlow = vm_pop(vm);
vm_push(vm, (dhigh == 0 && dlow == 0) ? -1 : 0);
}
/* D0< ( d -- flag ) */
void double_word_d_zero_less(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t dhigh = vm_pop(vm);
vm_pop(vm); // dlow (unused, but must pop)
vm_push(vm, (dhigh < 0) ? -1 : 0);
}
/* D2* ( d1 -- d2 ) */
void double_word_d_two_star(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t dhigh = vm_pop(vm);
cell_t dlow = vm_pop(vm);
// Left shift: new_low = dlow << 1, new_high = (dhigh << 1) | (carry from dlow)
unsigned long long udlow = (unsigned long long)(uint64_t) dlow;
cell_t new_dlow = (cell_t)(udlow << 1);
cell_t new_dhigh = (dhigh << 1) | ((udlow >> 63) & 1);
vm_push(vm, new_dlow);
vm_push(vm, new_dhigh);
}
/* D2/ ( d1 -- d2 ) */
void double_word_d_two_slash(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t dhigh = vm_pop(vm);
cell_t dlow = vm_pop(vm);
// Arithmetic right shift: preserve sign in high cell
cell_t new_dhigh = dhigh >> 1;
unsigned long long udlow = (unsigned long long)(uint64_t) dlow;
cell_t new_dlow = (cell_t)((udlow >> 1) | ((dhigh & 1) ? (1ULL << 63) : 0));
vm_push(vm, new_dlow);
vm_push(vm, new_dhigh);
}
/**
* @brief Register all double number words
*
* Registers all double-precision number operations with the virtual machine.
*
* @param vm Pointer to the virtual machine state
*
* @note This function must be called during VM initialization.
*/
void register_double_words(VM *vm) {
register_word(vm, "S>D", double_word_s_to_d);
register_word(vm, "D+", double_word_d_plus);
register_word(vm, "D-", double_word_d_minus);
register_word(vm, "DNEGATE", double_word_dnegate);
register_word(vm, "DABS", double_word_dabs);
register_word(vm, "DMAX", double_word_dmax);
register_word(vm, "DMIN", double_word_dmin);
register_word(vm, "D<", double_word_d_less);
register_word(vm, "D=", double_word_d_equals);
register_word(vm, "2DROP", double_word_2drop);
register_word(vm, "2DUP", double_word_2dup);
register_word(vm, "2SWAP", double_word_2swap);
register_word(vm, "2OVER", double_word_2over);
register_word(vm, "2ROT", double_word_2rot);
register_word(vm, "2>R", double_word_2to_r);
register_word(vm, "2R>", double_word_2r_from);
register_word(vm, "2R@", double_word_2r_fetch);
register_word(vm, "D0=", double_word_d_zero_equals);
register_word(vm, "D0<", double_word_d_zero_less);
register_word(vm, "D2*", double_word_d_two_star);
register_word(vm, "D2/", double_word_d_two_slash);
}
+344
View File
@@ -0,0 +1,344 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "include/editor_words.h" /* register_editor_words prototype */
#include "include/block_words.h" /* mark_buffer_dirty(), MAX_BLOCKS, BLOCK_SIZE */
#include "../../include/vm.h" /* VM, vm_load_cell, vm_store_cell, vm_addr_ok, vm_ptr */
#include "../../include/word_registry.h" /* register_word */
#include "../../include/log.h" /* log_message */
#include <stdio.h> /* fputs, fflush, puts, printf */
#include <string.h> /* memset, memcpy, strlen, isdigit, isspace */
#include <ctype.h> /* isdigit, isspace */
/* -------- helpers -------- */
/** @brief Get current screen number from VM
* @param vm Pointer to VM instance
* @return Current screen number
*/
static inline cell_t current_scr(VM *vm) {
return vm_load_cell(vm, vm->scr_addr);
}
/** @brief Get pointer to specific line in a screen
* @param vm Pointer to VM instance
* @param scr Screen number (1-based)
* @param line Line number (0-15)
* @param out Output parameter for line pointer
* @return 1 if successful, 0 if parameters invalid
*/
static int line_ptr(VM *vm, cell_t scr, cell_t line, unsigned char **out) {
if (!vm || !out) return 0;
if (scr < 1 || scr >= (cell_t) MAX_BLOCKS) return 0;
if (line < 0 || line > 15) return 0;
/* Use block subsystem to get block buffer */
extern uint8_t *blk_get_buffer(uint32_t block_num, int writable);
extern int blk_is_valid(uint32_t block_num);
if (!blk_is_valid((uint32_t) scr)) return 0;
uint8_t *block = blk_get_buffer((uint32_t) scr, 1); /* writable for editing */
if (!block) return 0;
*out = block + (line * 64);
return 1;
}
/** @brief Print 64-character line with character filtering
* @param p Pointer to 64-byte line buffer
*/
static void print_line_64(const unsigned char *p) {
#ifdef STARFORTH_ANSI
/* No cursor moves unless asked; just render content cleanly. */
#endif
for (int i = 0; i < 64; ++i) {
unsigned char c = p[i];
if (c == 0) { putchar(' '); } else if (c >= 32 && c <= 126) { putchar((int) c); } else {
putchar('.');
}
}
putchar('\n');
}
/* -------- words -------- */
/* L ( u -- ) : print line u of current SCR (0..15). Errors on empty/range/invalid SCR. */
static void editor_word_l(VM *vm) {
if (!vm) return;
if (vm->dsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "L: stack underflow (needs line# 0..15)");
return;
}
cell_t line = vm_pop(vm);
if (line < 0 || line > 15) {
vm->error = 1;
log_message(LOG_ERROR, "L: line out of range (0..15)");
return;
}
cell_t scr = current_scr(vm);
if (scr < 1 || scr >= (cell_t) MAX_BLOCKS) {
vm->error = 1;
log_message(LOG_ERROR, "L: SCR out of range (SCR=%ld)", (long) scr);
return;
}
unsigned char *lp = NULL;
if (!line_ptr(vm, scr, line, &lp)) {
vm->error = 1;
log_message(LOG_ERROR, "L: address invalid for SCR=%ld line=%ld", (long) scr, (long) line);
return;
}
print_line_64(lp);
}
/* S ( c-addr len u -- ) : set line u from buffer (pads with spaces to 64, truncates if longer). */
static void editor_word_s(VM *vm) {
if (!vm) return;
/* Pop in the order that lets us validate line range before address checks (useful in tests). */
if (vm->dsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "S: stack underflow (needs c-addr len line#)");
return;
}
cell_t line = vm_pop(vm);
if (line < 0 || line > 15) {
vm->error = 1;
log_message(LOG_ERROR, "S: line out of range (0..15)");
/* Drain remaining args to keep stack sane */
(void) vm_pop(vm);
(void) vm_pop(vm);
return;
}
cell_t len = vm_pop(vm);
cell_t addr = vm_pop(vm);
if (len < 0) len = 0; /* defensive: negative length treated as 0 */
cell_t scr = current_scr(vm);
if (scr < 1 || scr >= (cell_t) MAX_BLOCKS) {
vm->error = 1;
log_message(LOG_ERROR, "S: SCR out of range (SCR=%ld)", (long) scr);
return;
}
unsigned char *dst = NULL;
if (!line_ptr(vm, scr, line, &dst)) {
vm->error = 1;
log_message(LOG_ERROR, "S: destination address invalid");
return;
}
/* Source pointer */
if (!vm_addr_ok(vm, (vaddr_t) addr, (size_t) len)) {
vm->error = 1;
log_message(LOG_ERROR, "S: source range invalid (addr=%ld len=%ld)",
(long) addr, (long) len);
return;
}
const unsigned char *src = (const unsigned char *) vm_ptr(vm, (vaddr_t) addr);
/* Write: pad spaces, then copy up to 64 */
memset(dst, ' ', 64);
size_t n = (len > 64) ? 64 : (size_t) len;
if (n > 0) memcpy(dst, src, n);
mark_buffer_dirty(vm);
}
/* SHOW ( -- ) : print the whole 16x64 screen with simple line numbers. */
static void editor_word_show(VM *vm) {
if (!vm) return;
cell_t scr = current_scr(vm);
if (scr < 1 || scr >= (cell_t) MAX_BLOCKS) {
vm->error = 1;
log_message(LOG_ERROR, "SHOW: SCR out of range (SCR=%ld)", (long) scr);
return;
}
#ifdef STARFORTH_ANSI
/* Clear + home (optional; compile-time opt-in) */
fputs("\x1b[2J\x1b[H", stdout);
printf("\x1b[1mScreen %ld\x1b[0m:\n", (long) scr);
#else
printf("Screen %ld:\n", (long) scr);
#endif
for (cell_t line = 0; line < 16; ++line) {
unsigned char *lp = NULL;
if (!line_ptr(vm, scr, line, &lp)) {
vm->error = 1;
return;
}
#ifdef STARFORTH_ANSI
printf("\x1b[90m%2ld:\x1b[0m ", (long) line);
#else
printf("%2ld: ", (long) line);
#endif
print_line_64(lp);
}
}
/* EDIT ( u -- ) : tiny line-editor shell (stdin/stdout). No raw mode, no curses. */
/* EDIT ( u -- ) : tiny line-editor shell (stdin/stdout). No raw mode, no curses. */
static void editor_word_edit(VM *vm) {
if (!vm) return;
if (vm->dsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "EDIT: stack underflow (needs block#)");
return;
}
cell_t blk = vm_pop(vm);
if (blk < 1 || blk >= (cell_t) MAX_BLOCKS) {
vm->error = 1;
log_message(LOG_ERROR, "EDIT: block out of range");
return;
}
vm_store_cell(vm, vm->scr_addr, blk);
editor_word_show(vm);
if (vm->error) return;
char buf[1024];
for (;;) {
printf("SCR %ld> ", (long) vm_load_cell(vm, vm->scr_addr));
fflush(stdout);
if (!fgets(buf, sizeof buf, stdin)) break;
/* Trim trailing newline */
size_t L = strlen(buf);
if (L && buf[L - 1] == '\n') buf[L - 1] = '\0';
if (buf[0] == 'q' || buf[0] == 'Q') break;
else if (buf[0] == 'h' || buf[0] == 'H') {
puts("Commands: p=print, l <n>=line, s <n> <text>=set, n=next, b=prev, w=write, q=quit");
} else if (buf[0] == 'p' || buf[0] == 'P') {
editor_word_show(vm);
} else if (buf[0] == 'l' || buf[0] == 'L') {
long ln = -1;
/* Simple number parsing to avoid sscanf */
const char *p = buf + 1;
while (*p == ' ' || *p == '\t') p++;
if (isdigit(*p)) {
ln = 0;
while (isdigit(*p)) {
ln = ln * 10 + (*p - '0');
p++;
}
vm_push(vm, (cell_t) ln);
editor_word_l(vm);
} else {
puts("usage: l <0..15>");
}
} else if (buf[0] == 's' || buf[0] == 'S') {
long ln = -1;
const char *p = buf + 1;
while (*p && isspace((unsigned char) *p)) ++p;
/* Parse number manually */
if (isdigit(*p)) {
ln = 0;
while (isdigit(*p)) {
ln = ln * 10 + (*p - '0');
p++;
}
/* Move past any remaining spaces */
while (*p && isspace((unsigned char) *p)) ++p;
/* p is now the text to write */
size_t tlen = strlen(p);
cell_t scr = vm_load_cell(vm, vm->scr_addr);
unsigned char *dst = NULL;
if (ln < 0 || ln > 15 || scr < 1 || scr >= (cell_t) MAX_BLOCKS ||
!line_ptr(vm, scr, (cell_t) ln, &dst)) {
puts("error: bad SCR/line");
} else {
memset(dst, ' ', 64);
size_t n = (tlen > 64) ? 64 : tlen;
if (n) memcpy(dst, p, n);
/* ✅ Use helpers declared in block_words.h (no implicit decl) */
mark_buffer_dirty(vm);
puts("ok");
}
} else {
puts("usage: s <0..15> <text>");
}
} else if (buf[0] == 'n' || buf[0] == 'N') {
cell_t s = vm_load_cell(vm, vm->scr_addr);
if (s + 1 < (cell_t) MAX_BLOCKS) vm_store_cell(vm, vm->scr_addr, s + 1);
editor_word_show(vm);
} else if (buf[0] == 'b' || buf[0] == 'B') {
cell_t s = vm_load_cell(vm, vm->scr_addr);
if (s - 1 >= 1) vm_store_cell(vm, vm->scr_addr, s - 1);
editor_word_show(vm);
} else if (buf[0] == 'w' || buf[0] == 'W') {
/* ✅ No more implicit decls: use helpers */
mark_buffer_dirty(vm);
save_all_buffers(vm);
puts("saved");
} else {
puts("h for help");
}
if (vm->error) {
puts("error");
vm->error = 0; /* keep shell alive */
}
}
}
/* -------- registration -------- */
void register_editor_words(VM *vm) {
if (!vm) return;
register_word(vm, "L", editor_word_l); /* non-79 */
register_word(vm, "S", editor_word_s); /* non-79 */
register_word(vm, "SHOW", editor_word_show); /* non-79 */
register_word(vm, "EDIT", editor_word_edit); /* non-79 */
}
+480
View File
@@ -0,0 +1,480 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "include/format_words.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
#include "../../include/vm.h"
#include <stdint.h>
#include <limits.h>
#include <string.h>
#include <stdio.h>
/* Pictured-number hold buffer lives in each VM's own memory (hold_addr / hold_pos).
* These macros provide the same interface as the old static globals but are per-VM,
* so child VMs never alias Hera's buffer. */
#define PN_CAP 64
#define PN_BUF(vm) ((unsigned char *)((vm)->memory + (vm)->hold_addr))
#define CELL_BITS ((int)(sizeof(cell_t) * CHAR_BIT))
#define CHUNK_BITS 16
#define CHUNK_MASK ((uint64_t)(UINT64_C(1) << CHUNK_BITS) - 1)
/** @brief Returns current numeric conversion base, defaulting to 10 if invalid */
static unsigned current_base(VM *vm) {
unsigned b = (unsigned) vm->base;
return (b < 2 || b > 36) ? 10u : b;
}
/** @brief Converts value 0-35 to character '0'-'9' or 'A'-'Z' */
static char digit_for(unsigned v) {
return (v < 10) ? (char) ('0' + v) : (char) ('A' + (v - 10));
}
/* Divide unsigned double-cell (dhigh:dlow) by base (2..36), portable C99 */
/** @brief Divides unsigned double-cell number by base using portable C99 code */
static void div_ud_by_base(cell_t dhigh_in, cell_t dlow_in, unsigned base,
cell_t *qhigh_out, cell_t *qlow_out, unsigned *rem_out) {
uint64_t base_ul = (uint64_t) base;
uint64_t r = 0;
uint64_t qh = 0, ql = 0;
/* High word */
{
uint64_t hi = (uint64_t)(ucell_t) dhigh_in;
int parts = CELL_BITS / CHUNK_BITS;
for (int i = parts - 1; i >= 0; --i) {
uint64_t chunk = (hi >> (i * CHUNK_BITS)) & CHUNK_MASK;
uint64_t val = (r << CHUNK_BITS) + chunk;
uint64_t qchunk = val / base_ul;
r = val % base_ul;
qh = (qh << CHUNK_BITS) | (qchunk & CHUNK_MASK);
}
}
/* Low word */
{
uint64_t lo = (uint64_t)(ucell_t) dlow_in;
int parts = CELL_BITS / CHUNK_BITS;
for (int i = parts - 1; i >= 0; --i) {
uint64_t chunk = (lo >> (i * CHUNK_BITS)) & CHUNK_MASK;
uint64_t val = (r << CHUNK_BITS) + chunk;
uint64_t qchunk = val / base_ul;
r = val % base_ul;
ql = (ql << CHUNK_BITS) | (qchunk & CHUNK_MASK);
}
}
*qhigh_out = (cell_t) qh;
*qlow_out = (cell_t) ql;
*rem_out = (unsigned) r;
}
/* Render one cell in current base (no printf %x/%o so input==output base) */
/** @brief Prints number with optional width field and sign handling */
static void print_number_formatted(VM *vm, cell_t n, int width, int is_unsigned) {
char buf[80];
int i = 0;
unsigned base = current_base(vm);
if (!is_unsigned && n == 0) {
buf[i++] = '0';
} else if (is_unsigned) {
uint64_t u = (uint64_t)(ucell_t) n;
if (u == 0) buf[i++] = '0';
while (u != 0) {
uint64_t rem = u % (uint64_t) base;
u /= (uint64_t) base;
buf[i++] = digit_for((unsigned) rem);
}
} else {
int neg = (n < 0);
uint64_t u = (uint64_t)(ucell_t) n;
if (neg) u = (uint64_t) 0 - u;
if (u == 0) buf[i++] = '0';
while (u != 0) {
uint64_t rem = u % (uint64_t) base;
u /= (uint64_t) base;
buf[i++] = digit_for((unsigned) rem);
}
if (neg) buf[i++] = '-';
}
int len = i;
if (width > 0 && len < width) for (int s = 0; s < width - len; ++s) putchar(' ');
while (i--) putchar(buf[i]);
}
/* ===== Words ===== */
/* BASE ( -- addr ) — returns VM address (offset) of the BASE variable */
void format_word_base(VM *vm) {
if (!vm) { return; }
/* vm->base_addr is a VM address (offset into vm->memory), not a host pointer */
vm_push(vm, CELL(vm->base_addr));
}
/* DECIMAL ( -- ) — set BASE=10 */
void format_word_decimal(VM *vm) {
if (!vm) { return; }
vm_set_base(vm, 10);
}
/* HEX ( -- ) — set BASE=16 */
void format_word_hex(VM *vm) {
if (!vm) { return; }
vm_set_base(vm, 16);
}
/* OCTAL ( -- ) — set BASE=8 */
void format_word_octal(VM *vm) {
if (!vm) { return; }
vm_set_base(vm, 8);
}
/* <# ( -- ) */
void format_word_begin_conversion(VM *vm) {
vm->hold_pos = 0;
memset(PN_BUF(vm), 0, PN_CAP);
}
/* HOLD ( c -- ) */
void format_word_hold(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
log_message(LOG_ERROR, "HOLD: data stack underflow");
return;
}
cell_t c = vm_pop(vm);
/* Must be a single byte 0..255 */
if (c < 0 || c > 255) {
vm->error = 1;
log_message(LOG_ERROR, "HOLD: byte out of range (%ld)", (long) c);
return;
}
/* keep 1 byte of headroom (null safety) */
if (vm->hold_pos >= PN_CAP - 1) {
vm->error = 1;
log_message(LOG_ERROR, "HOLD: conversion buffer full (pos=%d cap=%d)", vm->hold_pos, PN_CAP);
return;
}
/* Prepend the byte */
memmove(&PN_BUF(vm)[1], &PN_BUF(vm)[0], (size_t) vm->hold_pos);
PN_BUF(vm)[0] = (unsigned char) c;
vm->hold_pos++;
}
/* SIGN ( n -- ) */
void format_word_sign(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
if (n < 0) {
vm_push(vm, '-');
format_word_hold(vm);
}
}
/* # ( ud1 | n -- ud2 )
Tolerant form:
- If only one cell is present, treat it as a signed single and convert its magnitude.
- SIGN (if used) is responsible for inserting '-' afterwards. */
void format_word_hash(VM *vm) {
cell_t dlow, dhigh;
/* If we don't have a full double, but have one cell, promote it. */
if (vm->dsp < 1) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
unsigned long u = (n < 0) ? (unsigned long) (-(cell_t) n) : (unsigned long) n;
dhigh = 0;
dlow = (cell_t) u;
} else {
/* Normal path: consume unsigned double (dlow dhigh) */
dlow = vm_pop(vm);
dhigh = vm_pop(vm);
}
unsigned base = current_base(vm);
cell_t qh, ql;
unsigned rem;
div_ud_by_base(dhigh, dlow, base, &qh, &ql, &rem);
/* Emit rightmost digit via HOLD */
vm_push(vm, (cell_t) digit_for(rem));
format_word_hold(vm);
if (vm->error) return;
/* Push quotient back as (dhigh dlow) so #S can loop */
vm_push(vm, qh);
vm_push(vm, ql);
}
/* #S ( ud | n -- 0 0 ) — tolerant:
If only one cell is present, treat it as a signed single and convert its magnitude.
SIGN (if used) is responsible for inserting '-' later. */
void format_word_hash_s(VM *vm) {
/* If we don't have a full double, but have one cell, promote it. */
if (vm->dsp < 1) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
/* Promote single to (dhigh=0, dlow=abs(n)) so # works on magnitude */
cell_t n = vm_pop(vm);
unsigned long u = (n < 0) ? (unsigned long) (-(cell_t) n) : (unsigned long) n;
/* Stack order for format_word_hash: top must be dlow, under it dhigh */
vm_push(vm, (cell_t) 0); /* dhigh */
vm_push(vm, (cell_t) u); /* dlow */
}
for (;;) {
/* Always convert at least once so 0 produces "0". */
format_word_hash(vm);
if (vm->error) return;
/* Peek the quotient (dhigh dlow) that '#' just pushed back */
cell_t dlow = vm->data_stack[vm->dsp];
cell_t dhigh = vm->data_stack[vm->dsp - 1];
if (dhigh == 0 && dlow == 0) break;
}
}
/* #> ( [ud] -- addr u ) — tolerant: pops ud iff present, else just returns buffer */
void format_word_end_conversion(VM *vm) {
/* If there’s at least a double on the stack, drop it (dlow dhigh). */
if (vm->dsp >= 1) {
(void) vm_pop(vm);
(void) vm_pop(vm);
}
/* Push VM address (offset) of the hold buffer and its fill count */
vm_push(vm, CELL(vm->hold_addr));
vm_push(vm, (cell_t) vm->hold_pos);
}
/* . ( n -- ) */
void format_word_dot(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t n = vm_pop(vm);
print_number_formatted(vm, n, 0, 0);
putchar(' ');
}
/* .R ( n width -- ) */
void format_word_dot_r(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t width = vm_pop(vm);
cell_t n = vm_pop(vm);
print_number_formatted(vm, n, (int) width, 0);
putchar(' ');
}
/* U. ( u -- ) */
void format_word_u_dot(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t u = vm_pop(vm);
print_number_formatted(vm, u, 0, 1);
putchar(' ');
}
/* U.R ( u width -- ) */
void format_word_u_dot_r(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t width = vm_pop(vm);
cell_t u = vm_pop(vm);
print_number_formatted(vm, u, (int) width, 1);
putchar(' ');
}
/* D. ( d -- ) */
void format_word_d_dot(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t dlow = vm_pop(vm);
cell_t dhigh = vm_pop(vm);
if ((dhigh == 0) || (dhigh == -1 && dlow < 0)) {
print_number_formatted(vm, dlow, 0, 0);
} else {
fputs("DOUBLE-OVERFLOW", stdout);
}
putchar(' ');
}
/* D.R ( d width -- ) */
void format_word_d_dot_r(VM *vm) {
if (vm->dsp < 2) {
vm->error = 1;
return;
}
cell_t width = vm_pop(vm);
cell_t dlow = vm_pop(vm);
cell_t dhigh = vm_pop(vm);
if ((dhigh == 0) || (dhigh == -1 && dlow < 0)) {
print_number_formatted(vm, dlow, (int) width, 0);
} else {
int len = (int) strlen("DOUBLE-OVERFLOW");
for (int i = 0; i < ((int) width > len ? (int) width - len : 0); ++i) putchar(' ');
fputs("DOUBLE-OVERFLOW", stdout);
}
putchar(' ');
}
/* .S ( -- ) */
void format_word_dot_s(VM *vm) {
printf("<%d> ", vm->dsp + 1);
for (int i = 0; i <= vm->dsp; i++) {
print_number_formatted(vm, vm->data_stack[i], 0, 0);
putchar(' ');
}
putchar('\n');
}
/* ? ( addr -- ) */
void format_word_question(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
vaddr_t addr = VM_ADDR(vm_pop(vm));
if (!vm_addr_ok(vm, addr, sizeof(cell_t))) {
vm->error = 1;
return;
}
print_number_formatted(vm, vm_load_cell(vm, addr), 0, 0);
putchar(' ');
}
/* DUMP ( addr u -- ) */
void format_word_dump(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t u = vm_pop(vm);
vaddr_t addr = VM_ADDR(vm_pop(vm));
if (u < 0) {
vm->error = 1;
return;
}
size_t n = (size_t) u;
if (!vm_addr_ok(vm, addr, n)) {
vm->error = 1;
return;
}
const uint8_t *ptr = vm_ptr(vm, addr);
int addr_width = (int) (sizeof(vaddr_t) * 2);
for (size_t i = 0; i < n; i += 16) {
unsigned long long a = (unsigned long long) (addr + i);
printf("%0*llX: ", addr_width, a);
size_t j = 0;
for (; j < 16 && i + j < n; j++) printf("%02X ", ptr[i + j]);
for (; j < 16; j++) printf(" ");
fputs(" |", stdout);
for (j = 0; j < 16 && i + j < n; j++) {
char c = (char) ptr[i + j];
putchar((c >= 32 && c <= 126) ? c : '.');
}
fputs("|\n", stdout);
}
}
/* Registration */
/**
* @brief Registers all formatting and numeric conversion words with the VM.
*
* This function registers the standard FORTH formatting words including:
* - Number output words (., .R, U., U.R, D., D.R, .S, ?)
* - Picture number formatting (<#, #, #S, #>, HOLD, SIGN)
* - Base conversion (BASE, DECIMAL, HEX, OCTAL)
* - Memory dump (DUMP)
*
* @param vm Pointer to the virtual machine instance
*/
void register_format_words(VM *vm) {
register_word(vm, ".", format_word_dot);
register_word(vm, ".R", format_word_dot_r);
register_word(vm, "U.", format_word_u_dot);
register_word(vm, "U.R", format_word_u_dot_r);
register_word(vm, "D.", format_word_d_dot);
register_word(vm, "D.R", format_word_d_dot_r);
register_word(vm, ".S", format_word_dot_s);
register_word(vm, "?", format_word_question);
register_word(vm, "DUMP", format_word_dump);
register_word(vm, "<#", format_word_begin_conversion);
register_word(vm, "#", format_word_hash);
register_word(vm, "#S", format_word_hash_s);
register_word(vm, "#>", format_word_end_conversion);
register_word(vm, "HOLD", format_word_hold);
register_word(vm, "SIGN", format_word_sign);
register_word(vm, "BASE", format_word_base);
register_word(vm, "DECIMAL", format_word_decimal);
register_word(vm, "HEX", format_word_hex);
register_word(vm, "OCTAL", format_word_octal);
}
+65
View File
@@ -0,0 +1,65 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
/* framebuffer_words.c — raw framebuffer hardware-boundary FORTH words
* (FABRIC-0.md item 4.3.3). Kernel-only; no-op on hosted builds. */
#include "include/framebuffer_words.h"
#include "../../include/log.h"
#include "../../include/word_registry.h"
#ifdef __STARKERNEL__
#include "starkernel/framebuffer.h"
#endif
/* PLOT ( x y color -- ) : raw raster pixel write, top-left origin, Y-down.
* No Cartesian awareness -- that is capsules/fabric.4th's job. */
static void fbw_plot(VM *vm)
{
if (vm->dsp < 2) {
log_message(LOG_ERROR, "PLOT: Stack underflow");
vm->error = 1;
return;
}
cell_t color = vm_pop(vm);
cell_t y = vm_pop(vm);
cell_t x = vm_pop(vm);
#ifdef __STARKERNEL__
fb_put_pixel((uint32_t)x, (uint32_t)y, (uint32_t)color);
#else
(void)x; (void)y; (void)color;
#endif
}
/* FB-WIDTH ( -- n ) */
static void fbw_width(VM *vm)
{
#ifdef __STARKERNEL__
vm_push(vm, (cell_t)fb_width());
#else
vm_push(vm, 0);
#endif
}
/* FB-HEIGHT ( -- n ) */
static void fbw_height(VM *vm)
{
#ifdef __STARKERNEL__
vm_push(vm, (cell_t)fb_height());
#else
vm_push(vm, 0);
#endif
}
void register_framebuffer_words(VM *vm)
{
register_word(vm, "PLOT", fbw_plot);
register_word(vm, "FB-WIDTH", fbw_width);
register_word(vm, "FB-HEIGHT", fbw_height);
}
+11
View File
@@ -0,0 +1,11 @@
# src/word_source/include/
Public headers for `src/word_source/`, one per `.c` file there (see
`src/word_source/README.md` for the category breakdown), plus
`control_words.h` and `mama_forth_words.h`:
- `control_words.h` — declarations for `control_words.c`'s `IF`/`THEN`/
`ELSE`/`BEGIN`/`DO`/`LOOP` family.
- `mama_forth_words.h` — declares the Mama VM word bindings implemented in
`src/starkernel/capsule/mama_forth_words.c` (kernel-only; the header
lives here for consistency with the rest of the word-category headers).
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include "vm.h"
/* Register all ACL C primitive words into the VM dictionary */
void register_acl_words(VM *vm);
/* Direct C entry point for birth inheritance (no stack manipulation needed) */
void acl_inherit_entry(DictEntry *src, DictEntry *dst);
@@ -0,0 +1,90 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef ARITHMETIC_WORDS_H
#define ARITHMETIC_WORDS_H
#include "vm.h"
/* FORTH-79 Arithmetic Words:
* + ( n1 n2 -- n3 ) Add n1 and n2
* - ( n1 n2 -- n3 ) Subtract n2 from n1
* * ( n1 n2 -- n3 ) Multiply n1 and n2
* / ( n1 n2 -- n3 ) Divide n1 by n2
* MOD ( n1 n2 -- n3 ) Remainder of n1/n2
* /MOD ( n1 n2 -- n3 n4 ) n3=remainder, n4=quotient of n1/n2
* NEGATE ( n1 -- n2 ) Negate n1
* ABS ( n1 -- n2 ) Absolute value of n1
* MIN ( n1 n2 -- n3 ) Minimum of n1 and n2
* MAX ( n1 n2 -- n3 ) Maximum of n1 and n2
* 1+ ( n1 -- n2 ) Add 1 to n1
* 1- ( n1 -- n2 ) Subtract 1 from n1
* 2* ( n1 -- n2 ) Multiply n1 by 2 (left shift)
* 2/ ( n1 -- n2 ) Divide n1 by 2 (right shift)
* STAR-SLASH ( n1 n2 n3 -- n4 ) Multiply n1*n2, divide by n3
*/
/**
* @brief Registers all FORTH-79 arithmetic words with the virtual machine
*
* Registers the following words:
* - + (add)
* - - (subtract)
* - * (multiply)
* - / (divide)
* - MOD (modulo)
* - /MOD (divide with remainder)
* - NEGATE
* - ABS (absolute)
* - MIN
* - MAX
* - 1+ (increment)
* - 1- (decrement)
* - 2* (multiply by 2)
* - 2/ (divide by 2)
* - STAR-SLASH (multiply then divide)
*
* @param vm Pointer to the Forth virtual machine instance
*/
void register_arithmetic_words(VM * vm);
#endif /* ARITHMETIC_WORDS_H */
+196
View File
@@ -0,0 +1,196 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file block_words.h
* @brief FORTH-79 Block and Buffer System Implementation
*
* This file implements the standard FORTH-79 block and buffer words:
* @li BLOCK ( u -- addr ) Get block buffer address
* @li BUFFER ( u -- addr ) Get empty buffer for block
* @li UPDATE ( -- ) Mark current block as modified
* @li SAVE-BUFFERS ( -- ) Save all modified blocks
* @li EMPTY-BUFFERS ( -- ) Mark all buffers as empty
* @li FLUSH ( -- ) Save buffers and empty them
* @li LIST ( u -- ) List block contents
* @li LOAD ( u -- ) Load and interpret block
* @li SCR ( -- addr ) Variable: screen/block being listed
* @li THRU ( u1 u2 -- ) Load blocks from u1 to u2
* @li --> or → ( -- ) Continue to next block
*/
#ifndef BLOCK_WORDS_H
#define BLOCK_WORDS_H
#include "vm.h"
/** @name Block System Configuration
* @{
*/
/** @brief Size of each block in bytes (1KB) */
#define BLOCK_SIZE 1024
/** @brief Maximum number of blocks (1024 blocks from 1MB memory) */
#define MAX_BLOCKS (VM_MEMORY_SIZE / BLOCK_SIZE)
/** @brief Number of block buffers in memory */
#define BUFFER_COUNT 4
/** @} */
/**
* @brief Block buffer types for different storage mechanisms
*/
typedef enum {
BUFFER_TYPE_MEMORY, /**< Direct memory access (current implementation) */
BUFFER_TYPE_HARDWARE, /**< Future: Hardware register blocks */
BUFFER_TYPE_STORAGE, /**< Future: Persistent storage blocks */
BUFFER_TYPE_NETWORK /**< Future: Network packet buffers */
} buffer_type_t;
/**
* @brief Block buffer structure maintaining buffer state
*/
typedef struct {
unsigned char data[BLOCK_SIZE]; /**< Block content buffer */
int block_number; /**< Currently loaded block number (-1 = empty) */
int dirty; /**< Modified flag for write-back */
int accessed; /**< LRU counter for buffer management */
buffer_type_t type; /**< Buffer type for storage mechanism */
uintptr_t hw_address; /**< Future: Hardware backing address */
} BlockBuffer;
/**
* @brief Registers all block and buffer words with the virtual machine
* @param vm Pointer to the Forth virtual machine instance
*/
void register_block_words(VM * vm);
/**
* @brief Initializes the block system
* @param vm Pointer to the Forth virtual machine instance
*/
void init_block_system(VM * vm);
/*
* @brief Gets a buffer containing the specified block
* @param vm Pointer to the Forth virtual machine instance
* @param block_num Block number to retrieve
* @return Pointer to the block buffer data
*/
unsigned char *get_block_buffer(VM *vm, int block_num);
/*
* @brief Gets an empty buffer for the specified block
* @param vm Pointer to the Forth virtual machine instance
* @param block_num Block number to allocate
* @return Pointer to the empty buffer data
*/
unsigned char *get_empty_buffer(VM *vm, int block_num);
/**
* @brief Marks the current buffer as modified
* @param vm Pointer to the Forth virtual machine instance
*/
void mark_buffer_dirty(VM * vm);
/*
* @brief Saves all modified buffers to storage
* @param vm Pointer to the Forth virtual machine instance
*/
void save_all_buffers(VM * vm);
/**
* @brief Sync every dirty VM block-window slot to the C-layer block cache,
* then flush the whole block subsystem to physical media --
* the complete SAVE-BUFFERS implementation (unlike
* save_all_buffers() above, which only calls blk_flush() and so
* misses any BUFFER-modified-but-not-yet-UPDATE'd content still
* sitting only in vm->memory's window). Cheap to call when nothing
* is dirty -- every check inside is a small fixed-size scan, no
* I/O happens unless something actually needs writing. Safe to
* call from outside word dispatch (e.g. a periodic idle-tick
* auto-flush); vm must be non-NULL.
* @param vm Pointer to the Forth virtual machine instance
*/
void blk_vm_flush_all(VM * vm);
/*
* @brief Marks all buffers as empty
* @param vm Pointer to the Forth virtual machine instance
*/
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 */
+127
View File
@@ -0,0 +1,127 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef CONTROL_WORDS_H
#define CONTROL_WORDS_H
#include "vm.h"
/**
* @defgroup control_words FORTH-79 Control Flow Words
* @{
* @brief Standard FORTH-79 control flow words implementation
*
* @def IF ( flag -- )
* Conditional execution based on flag
*
* @def THEN ( -- )
* End of IF conditional block
*
* @def ELSE ( -- )
* Alternative execution path
*
* @def BEGIN ( -- )
* Start of indefinite loop
*
* @def UNTIL ( flag -- )
* End loop if flag is true
*
* @def AGAIN ( -- )
* End infinite loop
*
* @def WHILE ( flag -- )
* Test condition in BEGIN loop
*
* @def REPEAT ( -- )
* End of WHILE loop
*
* @def DO ( limit start -- )
* Start definite loop
*
* @def LOOP ( -- )
* End DO loop, increment by 1
*
* @def +LOOP ( n -- )
* End DO loop, increment by n
*
* @def I ( -- n )
* Get current loop index
*
* @def J ( -- n )
* Get outer loop index
*
* @def LEAVE ( -- )
* Exit current loop
*
* @def UNLOOP ( -- )
* Discard loop parameters
*
* @def EXIT ( -- )
* Exit current word
*
* @def ABORT ( -- )
* Abort execution
*
* @def QUIT ( -- )
* Return to interpreter
*
* @def EXECUTE ( xt -- )
* Execute word at address
*
* @def BRANCH ( -- )
* Unconditional branch
*
* @def 0BRANCH ( flag -- )
* Branch if flag is zero
*
* @def (LIT) ( -- n )
* Push inline literal
* @}
*/
/**
* @brief Registers all control flow words with the Forth virtual machine
* @param vm Pointer to the Forth virtual machine instance
*/
void register_control_words(VM * vm);
#endif /* CONTROL_WORDS_H */
+17
View File
@@ -0,0 +1,17 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
#ifndef DEFER_WORDS_H
#define DEFER_WORDS_H
#include "../../../include/vm.h"
void register_defer_words(VM *vm);
#endif /* DEFER_WORDS_H */
@@ -0,0 +1,77 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef DEFINING_WORDS_H
#define DEFINING_WORDS_H
#include "vm.h"
/**
* @defgroup DefiningWords FORTH-79 Defining Words
* @{
* @brief Standard FORTH-79 defining words
*
* @details These words are used for creating new dictionary entries and defining words:
* - : ( -- ) Start colon definition
* - ; ( -- ) End colon definition
* - CREATE ( -- ) Create dictionary entry
* - DOES> ( -- ) Define runtime behavior
* - <BUILDS ( -- ) Create word with <BUILDS/DOES>
* - CONSTANT ( n -- ) Define constant
* - VARIABLE ( -- ) Define variable
* - 2CONSTANT ( d -- ) Define double constant
* - 2VARIABLE ( -- ) Define double variable
* - USER ( n -- ) Define user variable
* - IMMEDIATE ( -- ) Make most recent word immediate
* - [COMPILE] ( -- ) Force compilation of immediate word
* - COMPILE ( -- ) Compile inline code
* - FORGET ( -- ) Remove word and all after it
* @}
*/
/*
* @brief Registers all FORTH-79 defining words with the virtual machine
* @param vm Pointer to the virtual machine instance
*/
void register_defining_words(VM * vm);
#endif /* DEFINING_WORDS_H */

Some files were not shown because too many files have changed in this diff Show More