563 lines
18 KiB
C
563 lines
18 KiB
C
/*
|
||
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;
|
||
} |