Files
StarForth/src/word_source/string_words.c
T
Robert Allan JamesandClaude Sonnet 5 1a2ec565e8 Zero C-compiler warnings on all three architectures; fix real restore_vm_state() bug
Maintainability sweep (prompted by "this is getting hard to maintain"):
fixed the remaining three warning classes after the missing-field-
initializers commit -- 2x -Wsign-compare (control_words.c, cast at the
comparison site rather than changing cf_last_mode's type, which
deliberately holds a -999 sentinel outside vm_mode_t's valid range),
2x -Wstringop-truncation (mkcapsule.c, strncpy+manual-null-terminate
replaced with the idiomatic snprintf equivalent), and 26x
-Wunused-parameter (mostly documented stubs, silenced with the repo's
existing (void)param; idiom).

One of the unused-parameter warnings was not a deliberate stub -- a
real bug. restore_vm_state() (test_common.c) is named, documented, and
called by nine real call sites (acl_words_test.c x8 plus its own
internal use) as "restore saved VM state", but ignored all four of its
parameters and hard-reset to a fixed baseline instead, silently not
restoring what any caller actually saved. Fixed to actually assign the
passed-in dsp/rsp/error/mode. Found while fixing warnings, reported
before touching it, fixed/tested/documented/committed on explicit
instruction.

Verified: all three architectures build with zero C-compiler warnings
(amd64: 3040 -> 0; aarch64's one remaining note is lld-link's own
unrelated linker warning, not a C warning). Full amd64 acceptance boot
post-fix: POST 1003/965/0/0/38 (total/passed/failed/errors/stubs),
"ALL IMPLEMENTED TESTS PASSED!", contract checks (A4'/A1) all passed,
dict_hash=0x24b4279f0670aa3a -- an exact match to this document's own
previously-recorded baseline hash.

.claude/CLAUDE.md corrected to describe the real -Wno-error= exemption
list instead of the "-Wall -Werror" oversimplification. FABRIC-2.md
Section J records the full sweep, including doc-tree staleness findings
flagged but not fixed this pass (docs/lithosananke/ROADMAP.md branch
topology, docs/03-architecture/word-acl/DESIGN.md's Phase 7 claim
contradicting CLAUDE.md, top-level ROADMAP.md's stale StarForth-era
status, the Isabelle pipeline-metrics model mismatch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 22:31:57 -04:00

1023 lines
29 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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.
*/
/* string_words.c - FORTH-79 String & Text Processing Words */
#include "include/string_words.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
#include "../../include/vm.h"
#include "../../include/vm_api.h"
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
/* === Helpers ============================================================= */
/**
* @brief Convert a C pointer that points into vm->memory into a VM address (offset)
* @param vm Pointer to VM instance
* @param p Pointer into VM memory space
* @return Virtual address (offset) corresponding to the pointer
*/
static inline vaddr_t vaddr_from_ptr(VM *vm, const void *p) {
return (vaddr_t)((const uint8_t *) p - vm->memory);
}
/**
* @brief Ensure input buffers and variables are available
* @param vm Pointer to VM instance
* @return 0 on success, non-zero on error
*/
static inline int ensure_input(VM *vm) {
int err = vm_input_ensure(vm);
if (err) { vm->error = 1; }
return err;
}
/* Static VM-backed scratch buffer for WORD (allocated on first use) */
#define WORD_SCRATCH_CAP 64
static vaddr_t word_scratch_addr = 0;
/* Helper function to convert string to number (base-10 only here) */
static int convert_string_to_number(const char *str, size_t len, cell_t *result) {
char temp_str[32];
char *endptr;
long value;
if (len >= sizeof(temp_str)) return 0;
memcpy(temp_str, str, len);
temp_str[len] = '\0';
value = strtol(temp_str, &endptr, 10);
if (endptr != temp_str + len) return 0;
*result = (cell_t) value;
return 1;
}
/* === Words =============================================================== */
/**
* @brief COUNT ( addr1 -- addr2 u )
*
* Get string length and address. Converts a counted string at addr1
* into an address (addr2) and length (u) pair.
*
* @param vm Pointer to VM instance
*/
void string_word_count(VM *vm) {
if (vm->dsp < 0) {
log_message(LOG_ERROR, "COUNT: Data stack underflow");
vm->error = 1;
return;
}
cell_t addr1 = vm_pop(vm);
vaddr_t a = VM_ADDR(addr1);
if (!vm_addr_ok(vm, a, 1)) {
log_message(LOG_ERROR, "COUNT: Address out of bounds");
vm->error = 1;
return;
}
uint8_t count = vm_load_u8(vm, a);
if (!vm_addr_ok(vm, a + 1, (size_t) count)) {
log_message(LOG_ERROR, "COUNT: String extends beyond memory");
vm->error = 1;
return;
}
vm_push(vm, CELL(a + 1)); /* addr2 - skip count byte */
vm_push(vm, (cell_t) count); /* u - length */
}
/* EXPECT ( addr u -- ) Accept input line */
void string_word_expect(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t u = vm_pop(vm);
cell_t addr = vm_pop(vm);
if (u < 0) {
vm->error = 1;
return;
}
vaddr_t a = VM_ADDR(addr);
if (!vm_addr_ok(vm, a, (size_t) u)) {
vm->error = 1;
log_message(LOG_ERROR, "EXPECT: invalid buffer range");
return;
}
char *buffer = (char *) vm_ptr(vm, a);
if (!buffer) {
vm->error = 1;
return;
}
char *input = fgets(buffer, (int) u, stdin);
cell_t actual_len = 0;
if (input) {
size_t L = strlen(buffer);
if (L > 0 && buffer[L - 1] == '\n') {
buffer[L - 1] = '\0';
L--;
}
actual_len = (cell_t) L;
}
/* Update SPAN */
if (!ensure_input(vm)) {
cell_t *span = vm_input_span(vm);
if (span) *span = actual_len;
}
}
/* SPAN ( -- addr ) Variable: number of characters input (VM address) */
void string_word_span(VM *vm) {
if (ensure_input(vm)) return;
cell_t *p = vm_input_span(vm);
if (!p) {
vm->error = 1;
return;
}
vaddr_t v = vaddr_from_ptr(vm, p);
vm_push(vm, CELL(v));
}
/* QUERY ( -- ) Accept input into TIB */
void string_word_query(VM *vm) {
if (ensure_input(vm)) return;
unsigned char *tib = vm_input_tib(vm);
cell_t *inp = vm_input_in(vm);
cell_t *span = vm_input_span(vm);
if (!tib || !inp || !span) {
vm->error = 1;
return;
}
char *dst = (char *) tib;
char *input = fgets(dst, (int) INPUT_BUFFER_SIZE, stdin);
if (!input) {
tib[0] = 0;
*span = 0;
*inp = 0;
return;
}
size_t L = strlen(dst);
if (L > 0 && dst[L - 1] == '\n') {
dst[L - 1] = '\0';
L--;
}
*span = (cell_t) L;
*inp = 0;
}
/* TIB ( -- addr ) Terminal input buffer address (VM address) */
void string_word_tib(VM *vm) {
if (ensure_input(vm)) return;
unsigned char *p = vm_input_tib(vm);
if (!p) {
vm->error = 1;
return;
}
vaddr_t v = vaddr_from_ptr(vm, p);
vm_push(vm, CELL(v));
}
/* WORD ( c -- addr ) Parse next word, delimited by c (returns VM addr of counted string) */
void string_word_word(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
if (ensure_input(vm)) return;
cell_t c = vm_pop(vm);
char delimiter = (char) (c & 0xFF);
unsigned char *tib = vm_input_tib(vm);
cell_t *inp = vm_input_in(vm);
cell_t *span = vm_input_span(vm);
if (!tib || !inp || !span) {
vm->error = 1;
return;
}
size_t tib_len = (size_t) ((*span < 0) ? 0 : *span);
cell_t ip = (*inp < 0) ? 0 : *inp;
/* Skip leading delimiters */
while ((size_t) ip < tib_len && tib[ip] == (unsigned char) delimiter) ip++;
size_t start = (size_t) ip;
while ((size_t) ip < tib_len && tib[ip] != (unsigned char) delimiter) ip++;
size_t end = (size_t) ip;
size_t word_len = end - start;
if (word_len > WORD_SCRATCH_CAP - 2) word_len = WORD_SCRATCH_CAP - 2;
/* Allocate scratch once */
if (word_scratch_addr == 0) {
void *p = vm_allot(vm, WORD_SCRATCH_CAP);
if (!p) {
vm->error = 1;
return;
}
word_scratch_addr = vaddr_from_ptr(vm, p);
}
/* Build counted string at scratch */
uint8_t *wb = vm_ptr(vm, word_scratch_addr);
if (!wb) {
vm->error = 1;
return;
}
wb[0] = (uint8_t) word_len;
if (word_len) memcpy(&wb[1], &tib[start], word_len);
if (word_len + 1 < WORD_SCRATCH_CAP) wb[word_len + 1] = '\0'; /* safety */
/* Update >IN to char after the delimiter (if any) */
while ((size_t) ip < tib_len && tib[ip] == (unsigned char) delimiter) ip++;
*inp = ip;
vm_push(vm, CELL(word_scratch_addr));
}
/* >IN ( -- addr ) Input stream pointer cell (VM address) */
void string_word_to_in(VM *vm) {
if (ensure_input(vm)) return;
cell_t *p = vm_input_in(vm);
if (!p) {
vm->error = 1;
return;
}
vaddr_t v = vaddr_from_ptr(vm, p);
vm_push(vm, CELL(v));
}
/* SOURCE ( -- addr u ) Input source specification (TIB addr, length) */
void string_word_source(VM *vm) {
if (ensure_input(vm)) return;
unsigned char *tib = vm_input_tib(vm);
cell_t *span = vm_input_span(vm);
if (!tib || !span) {
vm->error = 1;
return;
}
vaddr_t v = vaddr_from_ptr(vm, tib);
cell_t len = (*span < 0) ? 0 : *span;
vm_push(vm, CELL(v));
vm_push(vm, len);
}
/* BL ( -- c ) ASCII space character (32) */
void string_word_bl(VM *vm) { vm_push(vm, 32); }
/* ['] ( -- xt ) IMMEDIATE — parse next word from input stream, compile XT as literal.
* In interpret mode behaves like ' (pushes XT). Uses input_buffer, not TIB. */
void string_word_bracket_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;
}
cell_t xt = (cell_t)(uintptr_t)e;
if (vm->mode == MODE_COMPILE) {
vm_compile_literal(vm, xt);
} else {
vm_push(vm, xt);
}
}
/* LITERAL / [LITERAL] are placeholders here */
void string_word_literal(VM *vm) {
(void)vm;
}
void string_word_bracket_literal(VM *vm) { string_word_literal(vm); }
/* CONVERT ( d1 addr1 -- d2 addr2 ) Convert string to number (very simplified) */
void string_word_convert(VM *vm) {
if (vm->dsp < 2) {
vm->error = 1;
return;
}
cell_t addr1 = vm_pop(vm);
cell_t dlow = vm_pop(vm);
cell_t dhigh = vm_pop(vm);
vaddr_t a = VM_ADDR(addr1);
if (!vm_addr_ok(vm, a, 1)) {
vm->error = 1;
return;
}
char *str_ptr = (char *) vm_ptr(vm, a);
if (!str_ptr) {
vm->error = 1;
return;
}
char *end_ptr = str_ptr;
cell_t digit_value;
for (size_t i = 0; str_ptr[i] != '\0'; i++) {
char c = str_ptr[i];
if (c >= '0' && c <= '9') digit_value = c - '0';
else if (c >= 'A' && c <= 'Z') digit_value = c - 'A' + 10;
else if (c >= 'a' && c <= 'z') digit_value = c - 'a' + 10;
else break;
if (digit_value >= 10) break; /* base 10 only here */
dlow = dlow * 10 + digit_value;
if (dlow < 0) dhigh++;
end_ptr++;
}
vm_push(vm, dhigh);
vm_push(vm, dlow);
/* return VM addr for end_ptr */
vaddr_t a2 = vaddr_from_ptr(vm, end_ptr);
vm_push(vm, CELL(a2));
}
/* NUMBER ( addr -- n flag ) Convert counted string to number (base-10 only) */
void string_word_number(VM *vm) {
if (vm->dsp < 0) {
vm->error = 1;
return;
}
cell_t addr = vm_pop(vm);
vaddr_t a = VM_ADDR(addr);
if (!vm_addr_ok(vm, a, 1)) {
vm_push(vm, 0);
vm_push(vm, 0);
return;
}
uint8_t *counted = vm_ptr(vm, a);
if (!counted) {
vm_push(vm, 0);
vm_push(vm, 0);
return;
}
uint8_t len = counted[0];
if (!vm_addr_ok(vm, a + 1, len)) {
vm_push(vm, 0);
vm_push(vm, 0);
return;
}
const char *data = (const char *) &counted[1];
cell_t result;
if (convert_string_to_number(data, (size_t) len, &result)) {
vm_push(vm, result);
vm_push(vm, 1);
} else {
vm_push(vm, 0);
vm_push(vm, 0);
}
}
/* ENCLOSE ( addr c -- addr1 n1 n2 n3 ) Parse for delimiter */
void string_word_enclose(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
return;
}
cell_t c = vm_pop(vm);
cell_t addr = vm_pop(vm);
vaddr_t a = VM_ADDR(addr);
if (!vm_addr_ok(vm, a, 1)) {
vm->error = 1;
return;
}
char *str = (char *) vm_ptr(vm, a);
if (!str) {
vm->error = 1;
return;
}
char delimiter = (char) (c & 0xFF);
size_t len = strlen(str);
size_t i = 0;
cell_t n1 = 0, n2 = 0, n3 = 0;
for (i = 0; i < len && str[i] == delimiter; i++) {
}
n1 = (cell_t) i;
for (; i < len && str[i] != delimiter; i++) {
}
n2 = (cell_t) i;
for (; i < len && str[i] == delimiter; i++) {
}
n3 = (cell_t) i;
vm_push(vm, addr); /* same addr back */
vm_push(vm, n1);
vm_push(vm, n2);
vm_push(vm, n3);
}
/* S" ( -- c-addr u ) — parse string literal, store at HERE, push addr+len */
/* (s") — runtime half of compiled S" ; reads inline [len][chars][pad] from
* the threaded-code stream via the return-stack IP, pushes ( c-addr u ), then
* advances IP past the padded block. NOT immediate.
*
* Inline layout immediately after the (s") cell in the thread:
* [1 length byte][n string bytes][padding to next cell boundary]
*/
static void string_runtime_s_quote(VM *vm) {
if (vm->rsp < 0) { vm->error = 1; return; }
uint8_t *data = (uint8_t *)(uintptr_t)vm->return_stack[vm->rsp];
if (!data) { vm->error = 1; return; }
uint8_t n = data[0];
/* c-addr points at the characters (byte after the length byte) */
vaddr_t caddr = vaddr_from_ptr(vm, data + 1);
/* advance IP past inline block */
size_t padded = (1 + (size_t)n + (sizeof(cell_t) - 1)) & ~(sizeof(cell_t) - 1);
vm->return_stack[vm->rsp] = (cell_t)(uintptr_t)(data + padded);
vm_push(vm, (cell_t)caddr);
vm_push(vm, (cell_t)n);
}
/* S" ( "ccc<quote>" -- c-addr u )
* Immediate. Interpretation: parse string, store at HERE, push c-addr u.
* Compilation: compile (s") followed by inline [len][chars][pad].
*/
static void string_word_s_quote(VM *vm) {
const char *src = vm->input_buffer;
size_t pos = vm->input_pos;
size_t end = vm->input_length;
if (!src || pos > end) {
vm->error = 1;
log_message(LOG_ERROR, "S\": input buffer not ready");
return;
}
/* Skip single leading space */
if (pos < end && src[pos] == ' ') pos++;
/* Collect until closing double-quote */
size_t start = pos;
while (pos < end && src[pos] != '"') pos++;
if (pos >= end) {
vm->error = 1;
log_message(LOG_ERROR, "S\": missing closing quote");
return;
}
size_t n = pos - start;
if (n > 255) {
vm->error = 1;
log_message(LOG_ERROR, "S\": string too long (%zu)", n);
return;
}
vm->input_pos = pos + 1; /* advance past closing quote */
if (vm->mode == MODE_COMPILE) {
/* Compile (s") + inline [len][chars][pad] — mirrors ." / (do-string) */
DictEntry *s_rt = vm_find_word(vm, "(s\")", 4);
if (!s_rt) { vm->error = 1; return; }
vm_compile_word(vm, s_rt);
size_t padded = (1 + n + (sizeof(cell_t) - 1)) & ~(sizeof(cell_t) - 1);
uint8_t *raw = (uint8_t *)vm_allot(vm, padded);
if (!raw) { vm->error = 1; return; }
raw[0] = (uint8_t)n;
for (size_t i = 0; i < n; i++) raw[1 + i] = (uint8_t)src[start + i];
for (size_t i = 1 + n; i < padded; i++) raw[i] = 0;
return;
}
/* Interpret mode: store at HERE, push ( c-addr u ) */
vaddr_t dst = (vaddr_t)vm->here;
if (!vm_addr_ok(vm, dst, n + 1)) {
vm->error = 1;
log_message(LOG_ERROR, "S\": dest out of bounds");
return;
}
for (size_t i = 0; i < n; i++)
vm_store_u8(vm, dst + i, (uint8_t)src[start + i]);
vm_store_u8(vm, dst + n, 0);
vm->here = (size_t)(dst + n + 1);
vm_push(vm, (cell_t)dst);
vm_push(vm, (cell_t)n);
log_message(LOG_DEBUG, "S\": %zu bytes at %zu", n, (size_t)dst);
}
/* -TRAILING ( addr u -- addr u' )
Trims trailing spaces from a string. Accepts:
- direct string at addr of length u, OR
- counted string at addr (first byte = u), auto-detected and adjusted. */
static void string_word_minus_trailing(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
log_message(LOG_ERROR, "-TRAILING: stack underflow");
return;
}
cell_t u = vm_pop(vm);
cell_t addr = vm_pop(vm);
if (u < 0) u = 0;
vaddr_t a = VM_ADDR(addr);
vaddr_t s = a; /* start of chars */
size_t n = (size_t) u; /* length of chars */
/* If this looks like a counted string and the region exists, adjust to chars */
if (n <= 255 && vm_addr_ok(vm, a, 1) && vm_load_u8(vm, a) == (uint8_t) n) {
if (!vm_addr_ok(vm, a + 1, n)) {
vm->error = 1;
log_message(LOG_ERROR, "-TRAILING: counted range OOB");
return;
}
s = a + 1;
} else {
if (!vm_addr_ok(vm, a, n)) {
vm->error = 1;
log_message(LOG_ERROR, "-TRAILING: range OOB");
return;
}
}
/* Trim trailing spaces (ASCII 32) */
while (n > 0) {
uint8_t ch = vm_load_u8(vm, s + (n - 1));
if (ch == 32) { n--; } else { break; }
}
vm_push(vm, CELL(s));
vm_push(vm, (cell_t) n);
}
/* CMOVE ( addr1 addr2 u -- )
Copy u bytes from addr1 to addr2 in ascending address order.
Works correctly for overlapping regions when addr2 >= addr1 (forward move).
All addresses are VM byte offsets. */
static void string_word_cmove(VM *vm) {
if (vm->dsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE: stack underflow");
return;
}
cell_t u = vm_pop(vm);
cell_t addr2 = vm_pop(vm);
cell_t addr1 = vm_pop(vm);
if (u < 0) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE: negative count");
return;
}
if (u == 0) return;
vaddr_t src = VM_ADDR(addr1);
vaddr_t dst = VM_ADDR(addr2);
size_t n = (size_t) u;
if (!vm_addr_ok(vm, src, n) || !vm_addr_ok(vm, dst, n)) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE: out-of-bounds (src=%ld dst=%ld n=%ld)",
(long) addr1, (long) addr2, (long) u);
return;
}
/* Ascending copy (low->high) */
for (size_t i = 0; i < n; i++) {
uint8_t b = vm_load_u8(vm, src + i);
vm_store_u8(vm, dst + i, b);
}
}
/* CMOVE> ( addr1 addr2 u -- )
Copy u bytes from addr1 to addr2 in descending address order (high->low).
Handles overlap correctly for dst < src. VM addresses (byte offsets). */
static void string_word_cmove_greater(VM *vm) {
if (vm->dsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE>: stack underflow");
return;
}
cell_t u = vm_pop(vm);
cell_t addr2 = vm_pop(vm);
cell_t addr1 = vm_pop(vm);
if (u < 0) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE>: negative count");
return;
}
if (u == 0) return;
vaddr_t src = VM_ADDR(addr1);
vaddr_t dst = VM_ADDR(addr2);
size_t n = (size_t) u;
if (!vm_addr_ok(vm, src, n) || !vm_addr_ok(vm, dst, n)) {
vm->error = 1;
log_message(LOG_ERROR, "CMOVE>: out-of-bounds (src=%ld dst=%ld n=%ld)",
(long) addr1, (long) addr2, (long) u);
return;
}
/* Descending copy (high->low) */
for (size_t i = n; i > 0; i--) {
uint8_t b = vm_load_u8(vm, src + (i - 1));
vm_store_u8(vm, dst + (i - 1), b);
}
}
/* COMPARE ( addr1 u1 addr2 u2 -- n )
Lexicographic compare, case-sensitive.
Returns -1 if s1<s2, 0 if equal, +1 if s1>s2.
Accepts either direct (addr/u) or counted strings at addr (when *addr == u). */
static void string_word_compare(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
log_message(LOG_ERROR, "COMPARE: stack underflow");
return;
}
cell_t u2 = vm_pop(vm);
cell_t addr2 = vm_pop(vm);
cell_t u1 = vm_pop(vm);
cell_t addr1 = vm_pop(vm);
if (u1 < 0) u1 = 0;
if (u2 < 0) u2 = 0;
vaddr_t a1 = VM_ADDR(addr1);
vaddr_t a2 = VM_ADDR(addr2);
/* detect counted string form (first byte equals u) and adjust */
vaddr_t s1 = a1;
size_t n1 = (size_t) u1;
if (n1 <= 255 && vm_addr_ok(vm, a1, 1) && vm_load_u8(vm, a1) == (uint8_t) n1) {
if (!vm_addr_ok(vm, a1 + 1, n1)) {
vm->error = 1;
log_message(LOG_ERROR, "COMPARE: s1 OOB");
return;
}
s1 = a1 + 1;
} else {
if (!vm_addr_ok(vm, a1, n1)) {
vm->error = 1;
log_message(LOG_ERROR, "COMPARE: s1 OOB");
return;
}
}
vaddr_t s2 = a2;
size_t n2 = (size_t) u2;
if (n2 <= 255 && vm_addr_ok(vm, a2, 1) && vm_load_u8(vm, a2) == (uint8_t) n2) {
if (!vm_addr_ok(vm, a2 + 1, n2)) {
vm->error = 1;
log_message(LOG_ERROR, "COMPARE: s2 OOB");
return;
}
s2 = a2 + 1;
} else {
if (!vm_addr_ok(vm, a2, n2)) {
vm->error = 1;
log_message(LOG_ERROR, "COMPARE: s2 OOB");
return;
}
}
/* byte-wise compare */
size_t m = (n1 < n2) ? n1 : n2;
int result = 0;
for (size_t i = 0; i < m; i++) {
uint8_t c1 = vm_load_u8(vm, s1 + i);
uint8_t c2 = vm_load_u8(vm, s2 + i);
if (c1 != c2) {
result = (c1 < c2) ? -1 : +1;
break;
}
}
if (result == 0) {
if (n1 < n2) result = -1;
else if (n1 > n2) result = +1;
else result = 0;
}
vm_push(vm, (cell_t) result);
}
/* SEARCH ( addr1 u1 addr2 u2 -- addr3 u3 flag )
Find first occurrence of s2 (addr2/u2) inside s1 (addr1/u1).
Returns:
- if found: addr3/u3 = tail of s1 starting at the match, flag = -1 (true)
- if not: addr3/u3 = original s1 (as chars), flag = 0
Accepts either direct (addr/u) or counted (addr points to count byte == u). */
static void string_word_search(VM *vm) {
if (vm->dsp < 3) {
vm->error = 1;
log_message(LOG_ERROR, "SEARCH: stack underflow");
return;
}
cell_t u2 = vm_pop(vm);
cell_t addr2 = vm_pop(vm);
cell_t u1 = vm_pop(vm);
cell_t addr1 = vm_pop(vm);
if (u1 < 0) u1 = 0;
if (u2 < 0) u2 = 0;
vaddr_t a1 = VM_ADDR(addr1);
vaddr_t a2 = VM_ADDR(addr2);
/* Convert to raw char spans, auto-detect counted strings */
vaddr_t s1 = a1;
size_t n1 = (size_t) u1;
if (n1 <= 255 && vm_addr_ok(vm, a1, 1) && vm_load_u8(vm, a1) == (uint8_t) n1) {
if (!vm_addr_ok(vm, a1 + 1, n1)) {
vm->error = 1;
log_message(LOG_ERROR, "SEARCH: s1 OOB");
return;
}
s1 = a1 + 1;
} else {
if (!vm_addr_ok(vm, a1, n1)) {
vm->error = 1;
log_message(LOG_ERROR, "SEARCH: s1 OOB");
return;
}
}
vaddr_t s2 = a2;
size_t n2 = (size_t) u2;
if (n2 <= 255 && vm_addr_ok(vm, a2, 1) && vm_load_u8(vm, a2) == (uint8_t) n2) {
if (!vm_addr_ok(vm, a2 + 1, n2)) {
vm->error = 1;
log_message(LOG_ERROR, "SEARCH: s2 OOB");
return;
}
s2 = a2 + 1;
} else {
if (!vm_addr_ok(vm, a2, n2)) {
vm->error = 1;
log_message(LOG_ERROR, "SEARCH: s2 OOB");
return;
}
}
/* Edge cases (ANS/FORTH-79 style): empty needle is found at start */
if (n2 == 0) {
vm_push(vm, CELL(s1));
vm_push(vm, (cell_t) n1);
vm_push(vm, (cell_t) - 1);
return;
}
if (n2 > n1) {
vm_push(vm, CELL(s1));
vm_push(vm, (cell_t) n1);
vm_push(vm, 0);
return;
}
/* Naive first-occurrence search */
size_t limit = n1 - n2;
for (size_t i = 0; i <= limit; i++) {
size_t j = 0;
for (; j < n2; j++) {
uint8_t c1 = vm_load_u8(vm, s1 + i + j);
uint8_t c2 = vm_load_u8(vm, s2 + j);
if (c1 != c2) break;
}
if (j == n2) {
/* Found: tail of s1 starting at match */
vm_push(vm, CELL(s1 + i));
vm_push(vm, (cell_t)(n1 - i));
vm_push(vm, (cell_t) - 1);
return;
}
}
/* Not found: return original s1 span and false */
vm_push(vm, CELL(s1));
vm_push(vm, (cell_t) n1);
vm_push(vm, 0);
}
/* SCAN ( addr u char -- addr' u' )
Find first occurrence of char in the string. If found, return the tail
starting at that character; else return end-of-string and 0.
Accepts either direct (addr/u) or counted strings at addr (when *addr == u). */
static void string_word_scan(VM *vm) {
if (vm->dsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "SCAN: stack underflow");
return;
}
cell_t ch = vm_pop(vm);
cell_t u = vm_pop(vm);
cell_t addr = vm_pop(vm);
if (u < 0) u = 0;
vaddr_t a = VM_ADDR(addr);
vaddr_t s = a;
size_t n = (size_t) u;
/* auto-detect counted string form (first byte equals u) */
if (n <= 255 && vm_addr_ok(vm, a, 1) && vm_load_u8(vm, a) == (uint8_t) n) {
if (!vm_addr_ok(vm, a + 1, n)) {
vm->error = 1;
log_message(LOG_ERROR, "SCAN: counted range OOB");
return;
}
s = a + 1;
} else {
if (!vm_addr_ok(vm, a, n)) {
vm->error = 1;
log_message(LOG_ERROR, "SCAN: range OOB");
return;
}
}
uint8_t needle = (uint8_t)(ch & 0xFF);
/* search */
size_t i = 0;
for (; i < n; i++) {
if (vm_load_u8(vm, s + i) == needle) break;
}
if (i < n) {
vm_push(vm, CELL(s + i));
vm_push(vm, (cell_t)(n - i));
} else {
vm_push(vm, CELL(s + n));
vm_push(vm, 0);
}
}
/* SKIP ( addr u char -- addr' u' )
Skip leading occurrences of char in the string.
Accepts either direct (addr/u) or counted strings at addr (when *addr == u). */
static void string_word_skip(VM *vm) {
if (vm->dsp < 2) {
vm->error = 1;
log_message(LOG_ERROR, "SKIP: stack underflow");
return;
}
cell_t ch = vm_pop(vm);
cell_t u = vm_pop(vm);
cell_t addr = vm_pop(vm);
if (u < 0) u = 0;
vaddr_t a = VM_ADDR(addr);
vaddr_t s = a;
size_t n = (size_t) u;
/* auto-detect counted string form (first byte equals u) */
if (n <= 255 && vm_addr_ok(vm, a, 1) && vm_load_u8(vm, a) == (uint8_t) n) {
if (!vm_addr_ok(vm, a + 1, n)) {
vm->error = 1;
log_message(LOG_ERROR, "SKIP: counted range OOB");
return;
}
s = a + 1;
} else {
if (!vm_addr_ok(vm, a, n)) {
vm->error = 1;
log_message(LOG_ERROR, "SKIP: range OOB");
return;
}
}
uint8_t needle = (uint8_t)(ch & 0xFF);
size_t i = 0;
while (i < n && vm_load_u8(vm, s + i) == needle) i++;
vm_push(vm, CELL(s + i));
vm_push(vm, (cell_t)(n - i));
}
/* BLANK ( addr u -- )
Fill u bytes with ASCII space (32). If addr looks like a counted string
(first byte equals u and there’s room), we blank the character area (addr+1,u). */
static void string_word_blank(VM *vm) {
if (vm->dsp < 1) {
vm->error = 1;
log_message(LOG_ERROR, "BLANK: stack underflow");
return;
}
cell_t u = vm_pop(vm);
cell_t addr = vm_pop(vm);
if (u < 0) {
vm->error = 1;
log_message(LOG_ERROR, "BLANK: negative length");
return;
}
if (u == 0) return;
vaddr_t a = VM_ADDR(addr);
size_t n = (size_t) u;
/* If it looks like a counted string, operate on the chars, not the count byte */
vaddr_t s = a;
if (n <= 255 && vm_addr_ok(vm, a, 1) && vm_load_u8(vm, a) == (uint8_t) n) {
if (!vm_addr_ok(vm, a + 1, n)) {
vm->error = 1;
log_message(LOG_ERROR, "BLANK: counted range OOB");
return;
}
s = a + 1;
} else {
if (!vm_addr_ok(vm, a, n)) {
vm->error = 1;
log_message(LOG_ERROR, "BLANK: range OOB");
return;
}
}
for (size_t i = 0; i < n; i++) vm_store_u8(vm, s + i, 32);
}
/* FORTH-79 String Word Registration */
void register_string_words(VM *vm) {
register_word(vm, "COUNT", string_word_count);
register_word(vm, "EXPECT", string_word_expect);
register_word(vm, "SPAN", string_word_span);
register_word(vm, "QUERY", string_word_query);
register_word(vm, "TIB", string_word_tib);
register_word(vm, "WORD", string_word_word);
register_word(vm, "(s\")", string_runtime_s_quote);
register_word(vm, "S\"", string_word_s_quote);
vm_make_immediate(vm);
register_word(vm, ">IN", string_word_to_in);
register_word(vm, "SOURCE", string_word_source);
register_word(vm, "BL", string_word_bl);
// ' (tick) word is registered in dictionary_manipulation_words.c
register_word(vm, "[']", string_word_bracket_tick);
vm_make_immediate(vm); /* ['] is IMMEDIATE: compiles XT as literal at define time */
register_word(vm, "LITERAL", string_word_literal);
register_word(vm, "[LITERAL]", string_word_bracket_literal);
register_word(vm, "CONVERT", string_word_convert);
register_word(vm, "NUMBER", string_word_number);
register_word(vm, "ENCLOSE", string_word_enclose);
register_word(vm, "-TRAILING", string_word_minus_trailing);
register_word(vm, "CMOVE", string_word_cmove);
register_word(vm, "CMOVE>", string_word_cmove_greater);
register_word(vm, "COMPARE", string_word_compare);
register_word(vm, "SEARCH", string_word_search);
register_word(vm, "SCAN", string_word_scan);
register_word(vm, "SKIP", string_word_skip);
register_word(vm, "BLANK", string_word_blank);
}