From 08c5918cd7f81c1ef3c8890e18d484d2d99aaef7 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Tue, 22 Sep 2026 14:58:56 +0300 Subject: [PATCH 01/13] build: Windows (MSYS2 CLANG64/MINGW64) toolchain in the Makefile Detect Windows via $(OS) or uname (MSYS2's make hides $(OS)), link Winsock and a statically linked winpthreads, build with 64-bit off_t (_FILE_OFFSET_BITS=64) and an 8 MiB stack like a Linux main thread. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index aa6b666e..f6c170f3 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,25 @@ COVERAGE_CFLAGS = -fPIC $(WARNS) -std=$(STD) -g -O0 -march=$(RAY_MARCH) -DDEBUG -fno-omit-frame-pointer -fprofile-instr-generate -fcoverage-mapping COVERAGE_LDFLAGS = -fprofile-instr-generate -fcoverage-mapping -ifeq ($(UNAME_S),Linux) +# Windows: MSYS2 CLANG64/MINGW64 toolchain (x86_64-w64-windows-gnu). MSYS2's +# own make hides $(OS), so also match `uname -s` (MINGW64_NT-*, CLANG64_NT-*, +# MSYS_NT-*); a native make started from PowerShell/cmd sees OS=Windows_NT. +RAY_WINDOWS := $(if $(filter Windows_NT,$(OS))$(findstring _NT-,$(UNAME_S)),1,) + +ifeq ($(RAY_WINDOWS),1) + # 64-bit off_t / struct stat.st_size: MinGW defaults both to 32 bits, which + # silently truncates sizes of files over 2 GiB (stat, lseek, ftruncate). + DEFS += -D_FILE_OFFSET_BITS=64 + # --stack: 8 MiB like a Linux main thread (the Windows default is 1 MiB, + # too little for deep DAG/eval recursion). CreateThread(size 0) inherits + # it too, so pool workers get the same. winpthreads (sched_yield, + # clock_gettime) is linked statically so the binary needs no MSYS2 DLL; the + # UCRT it also uses ships with Windows 10+. + LIBS = -lws2_32 -lmswsock -lkernel32 -ladvapi32 \ + -Wl,-Bstatic -lpthread -Wl,-Bdynamic \ + -Wl,--stack,8388608 + RELEASE_LDFLAGS = -Wl,--gc-sections +else ifeq ($(UNAME_S),Linux) LIBS = -lm -lpthread RELEASE_LDFLAGS = -Wl,--gc-sections -Wl,--as-needed else From 5ee60ea89b0702c3d708256f5d4eaf4b6b7bcd5e Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Tue, 22 Sep 2026 14:58:56 +0300 Subject: [PATCH 02/13] feat(core): WSAPoll event loop for Windows Replace the IOCP stub with a readiness-based loop that mirrors the epoll backend's dispatch order, so the selector state machine is identical on every platform. stdin (console or pipe) is not a socket, so RAY_SEL_STDIN selectors are probed directly and the socket wait is sliced while one is registered. Co-Authored-By: Claude Opus 5 (1M context) --- src/core/iocp.c | 431 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 416 insertions(+), 15 deletions(-) diff --git a/src/core/iocp.c b/src/core/iocp.c index 636c3fed..dd75fd8f 100644 --- a/src/core/iocp.c +++ b/src/core/iocp.c @@ -21,36 +21,181 @@ * SOFTWARE. */ +#include "core/platform.h" + #if defined(RAY_OS_WINDOWS) +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include + #include "core/poll.h" -#include +#include "core/mcast.h" +#include "core/timer.h" +#include "mem/sys.h" +#include "mem/heap.h" /* idle decay: bound the wait, sweep after wakeup */ + +/* Windows event loop. + * + * Readiness-based, like the epoll and kqueue backends, so the selector + * state machine (rx fill -> read_fn -> data_fn, tx flush) is the same on + * every platform. Sockets are waited on with WSAPoll. Standard input is + * not a socket and WSAPoll cannot watch it, so RAY_SEL_STDIN selectors are + * probed directly (console input queue, bytes in a pipe) and the socket + * wait is cut into short slices while one is registered. + * + * WSAPoll keeps no kernel-side registration, so the wait set is rebuilt + * from poll->sels on every pass: register/deregister and tx request/cancel + * need no OS call, and a selector's write interest simply follows whether + * it has a pending tx buffer. */ + +#define RAY_POLL_INITIAL_CAP 16 +#define RAY_POLL_STDIN_SLICE_MS 10 + +enum { EV_IN = 1, EV_OUT = 2, EV_HUP = 4 }; + +/* ===== stdin readiness ===== */ + +/* Would a read of `fd` return now (data or EOF) instead of blocking? */ +static int stdin_events(int64_t fd) +{ + HANDLE h = (HANDLE)_get_osfhandle((int)fd); + if (h == INVALID_HANDLE_VALUE) return EV_HUP; + + switch (GetFileType(h)) { + case FILE_TYPE_CHAR: { + DWORD mode; + if (!GetConsoleMode(h, &mode)) return EV_IN; /* NUL device: never blocks */ + for (;;) { + INPUT_RECORD rec; + DWORD n = 0; + if (!PeekConsoleInputW(h, &rec, 1, &n)) return EV_HUP; + if (n == 0) return 0; + if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown && + rec.Event.KeyEvent.uChar.UnicodeChar != 0) + return EV_IN; + /* Focus, mouse, resize and key-up records never yield a byte to + * ReadFile: drop them, or a "ready" console would make the + * reader block in ReadFile until the next real keystroke. */ + if (!ReadConsoleInputW(h, &rec, 1, &n)) return EV_HUP; + } + } + case FILE_TYPE_PIPE: { + DWORD avail = 0; + if (!PeekNamedPipe(h, NULL, 0, NULL, &avail, NULL)) + return EV_IN | EV_HUP; /* writer gone: the read returns EOF */ + return avail ? EV_IN : 0; + } + default: + return EV_IN; /* disk file: reads never block */ + } +} -/* Windows IOCP implementation — stub for now. - * Full IOCP support is deferred to a future release. */ +/* ===== Lifecycle ===== */ ray_poll_t* ray_poll_create(void) { - fprintf(stderr, "ray_poll_create: IOCP not yet implemented\n"); - return NULL; + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return NULL; + + ray_poll_t* poll = (ray_poll_t*)ray_sys_alloc(sizeof(ray_poll_t)); + if (!poll) return NULL; + + memset(poll, 0, sizeof(*poll)); + poll->fd = -1; /* no kernel object behind a WSAPoll loop */ + poll->code = -1; + poll->sel_cap = RAY_POLL_INITIAL_CAP; + poll->sels = (ray_selector_t**)ray_sys_alloc( + poll->sel_cap * sizeof(ray_selector_t*)); + if (!poll->sels) { + ray_sys_free(poll); + return NULL; + } + memset(poll->sels, 0, poll->sel_cap * sizeof(ray_selector_t*)); + return poll; } void ray_poll_destroy(ray_poll_t* poll) { - (void)poll; + if (!poll) return; + + for (uint32_t i = 0; i < poll->n_sels; i++) { + ray_selector_t* sel = poll->sels[i]; + if (!sel) continue; + if (sel->close_fn) sel->close_fn(poll, sel); + if (sel->rx.buf) ray_poll_buf_free(sel->rx.buf); + ray_poll_buf_free(sel->tx.buf); + ray_sys_free(sel); + poll->sels[i] = NULL; + } + + if (poll->sels) ray_sys_free(poll->sels); + if (poll->timers) { + ray_timers_destroy((ray_timers_t*)poll->timers); + poll->timers = NULL; + } + if (poll->mcast) { + ray_mcast_destroy((ray_mcast_t*)poll->mcast); + poll->mcast = NULL; + } + ray_sys_free(poll); } +/* ===== Registration ===== */ + int64_t ray_poll_register(ray_poll_t* poll, ray_poll_reg_t* reg) { - (void)poll; (void)reg; - return -1; -} + if (!poll || !reg) return -1; -void ray_poll_deregister(ray_poll_t* poll, int64_t id) -{ - (void)poll; (void)id; + /* Find free slot or grow */ + int64_t id = -1; + for (uint32_t i = 0; i < poll->n_sels; i++) { + if (!poll->sels[i]) { id = (int64_t)i; break; } + } + if (id < 0) { + if (poll->n_sels >= poll->sel_cap) { + uint32_t new_cap = poll->sel_cap * 2; + ray_selector_t** ns = (ray_selector_t**)ray_sys_alloc( + new_cap * sizeof(ray_selector_t*)); + if (!ns) return -1; + memcpy(ns, poll->sels, poll->n_sels * sizeof(ray_selector_t*)); + memset(ns + poll->n_sels, 0, + (new_cap - poll->n_sels) * sizeof(ray_selector_t*)); + ray_sys_free(poll->sels); + poll->sels = ns; + poll->sel_cap = new_cap; + } + id = (int64_t)poll->n_sels; + poll->n_sels++; + } + + ray_selector_t* sel = (ray_selector_t*)ray_sys_alloc(sizeof(ray_selector_t)); + if (!sel) return -1; + memset(sel, 0, sizeof(*sel)); + + sel->fd = reg->fd; + sel->id = id; + sel->type = reg->type; + sel->data = reg->data; + sel->open_fn = reg->open_fn; + sel->close_fn = reg->close_fn; + sel->error_fn = reg->error_fn; + sel->data_fn = reg->data_fn; + sel->rx.recv_fn = reg->recv_fn; + sel->rx.read_fn = reg->read_fn; + sel->tx.send_fn = reg->send_fn; + + poll->sels[id] = sel; + poll->n_live++; + if (sel->open_fn) sel->open_fn(poll, sel); + return id; } +/* Write interest is derived from sel->tx.buf when the wait set is built. */ void ray_poll_tx_request(ray_poll_t* poll, ray_selector_t* sel) { (void)poll; (void)sel; @@ -61,10 +206,266 @@ void ray_poll_tx_cancel(ray_poll_t* poll, ray_selector_t* sel) (void)poll; (void)sel; } +void ray_poll_deregister(ray_poll_t* poll, int64_t id) +{ + if (!poll || id < 0 || (uint32_t)id >= poll->n_sels) return; + ray_selector_t* sel = poll->sels[id]; + if (!sel) return; + + if (sel->close_fn) sel->close_fn(poll, sel); + if (sel->rx.buf) ray_poll_buf_free(sel->rx.buf); + ray_poll_buf_free(sel->tx.buf); + ray_sys_free(sel); + poll->sels[id] = NULL; + if (poll->n_live > 0) poll->n_live--; +} + +/* ===== Event dispatch (same order and rules as the epoll backend) ===== */ + +static void poll_dispatch(ray_poll_t* poll, uint64_t eid, int events) +{ + ray_selector_t* sel = NULL; + if (eid < poll->n_sels) + sel = poll->sels[eid]; + if (!sel) return; + + /* Process readable data first — even if hangup is also set. A client + * may send a message and close; both arrive in the same wakeup. */ + if (events & EV_IN) { + /* Loop: read data -> call read_fn -> if state advanced, read more. + * Handles multi-phase protocols (handshake -> header -> payload) + * arriving in a single wakeup. */ + for (;;) { + if (sel->rx.recv_fn && sel->rx.buf) { + while (sel->rx.buf->offset < sel->rx.buf->size) { + int64_t nr = sel->rx.recv_fn( + sel->fd, + sel->rx.buf->data + sel->rx.buf->offset, + sel->rx.buf->size - sel->rx.buf->offset); + if (nr <= 0) { + if (nr < 0 && errno == EINTR) continue; + if (nr < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) + break; + /* Error or peer closed mid-read */ + if (sel->error_fn) + sel->error_fn(poll, sel); + else + ray_poll_deregister(poll, sel->id); + return; + } + sel->rx.buf->offset += nr; + } + } + + /* Not enough data for current phase */ + if (sel->rx.buf && sel->rx.buf->offset < sel->rx.buf->size) + break; + + /* Call read_fn — may advance state and request new buffer */ + if (!sel->rx.read_fn) break; + ray_t* obj = sel->rx.read_fn(poll, sel); + + /* Re-validate: read_fn may have deregistered this selector */ + if (eid >= poll->n_sels || !poll->sels[eid]) return; + sel = poll->sels[eid]; + + if (obj && sel->data_fn) + sel->data_fn(poll, sel, obj); + + if (eid >= poll->n_sels || !poll->sels[eid]) return; + sel = poll->sels[eid]; + + /* If no rx buffer (state machine done or not set), stop */ + if (!sel->rx.buf) break; + /* If buffer already has enough data for next phase, loop */ + if (sel->rx.buf->offset >= sel->rx.buf->size) continue; + /* Otherwise try reading more (may EAGAIN -> break) */ + } + } + + if (events & EV_OUT) { + if (eid >= poll->n_sels || !poll->sels[eid]) return; + sel = poll->sels[eid]; + if (sel->tx.buf && ray_poll_tx_flush(poll, sel) < 0) { + if (eid < poll->n_sels && poll->sels[eid]) { + sel = poll->sels[eid]; + if (sel->error_fn) + sel->error_fn(poll, sel); + else + ray_poll_deregister(poll, sel->id); + } + return; + } + } + + /* Error / hangup — after data is drained */ + if (events & EV_HUP) { + if (eid < poll->n_sels && poll->sels[eid]) { + sel = poll->sels[eid]; + if (sel->error_fn) + sel->error_fn(poll, sel); + else + ray_poll_deregister(poll, sel->id); + } + } +} + +/* ===== Run loop ===== */ + int64_t ray_poll_run_for(ray_poll_t* poll, int timeout_ms) { - (void)poll; (void)timeout_ms; - return -1; + if (!poll) return -1; + + bool bounded = timeout_ms >= 0; + int64_t end_ms = bounded ? ray_time_now_ms() + timeout_ms : INT64_MAX; + + while (poll->code < 0) { + int wait_ms = -1; + if (bounded) { + int64_t remaining = end_ms - ray_time_now_ms(); + if (remaining < 0) remaining = 0; + if (remaining > INT_MAX) remaining = INT_MAX; + wait_ms = (int)remaining; + } + + /* Nothing registered and nothing scheduled: an unbounded loop + * would block here forever. Return instead, so a process that + * stayed only for its timers can end once they are spent. */ + if (!bounded && ray_poll_idle(poll)) return 0; + + if (poll->timers) { + int64_t deadline = ray_timers_next_deadline_ms( + (ray_timers_t*)poll->timers); + if (deadline != INT64_MAX) { + int64_t delta = deadline - ray_time_now_ms(); + if (delta < 0) delta = 0; + if (delta > INT_MAX) delta = INT_MAX; + if (wait_ms < 0 || delta < wait_ms) + wait_ms = (int)delta; + } + } + + /* Idle allocator decay bounds an unbounded wait, exactly as in the + * epoll backend (see the comment there). */ + { + int64_t decay = bounded ? -1 : ray_heap_decay_due_ms(); + if (decay >= 0) { + if (decay > INT_MAX) decay = INT_MAX; + if (wait_ms < 0 || decay < wait_ms) wait_ms = (int)decay; + } + } + + /* Build this pass's wait set: sockets for WSAPoll, stdin probed. */ + uint32_t cap = poll->n_sels ? poll->n_sels : 1; + WSAPOLLFD* pfds = (WSAPOLLFD*)ray_sys_alloc(cap * sizeof(WSAPOLLFD)); + uint32_t* pids = (uint32_t*)ray_sys_alloc(cap * sizeof(uint32_t)); + uint32_t* sids = (uint32_t*)ray_sys_alloc(cap * sizeof(uint32_t)); + int* sevs = (int*)ray_sys_alloc(cap * sizeof(int)); + if (!pfds || !pids || !sids || !sevs) { + if (pfds) ray_sys_free(pfds); + if (pids) ray_sys_free(pids); + if (sids) ray_sys_free(sids); + if (sevs) ray_sys_free(sevs); + return -1; + } + + uint32_t nfds = 0, nstd = 0; + for (uint32_t i = 0; i < poll->n_sels; i++) { + ray_selector_t* sel = poll->sels[i]; + if (!sel) continue; + if (sel->type == RAY_SEL_STDIN) { + sids[nstd] = i; + sevs[nstd] = 0; + nstd++; + continue; + } + pfds[nfds].fd = (SOCKET)sel->fd; + pfds[nfds].events = POLLRDNORM | (sel->tx.buf ? POLLWRNORM : 0); + pfds[nfds].revents = 0; + pids[nfds] = i; + nfds++; + } + + /* Wait. With stdin registered the socket wait is sliced so the + * console/pipe is re-probed every RAY_POLL_STDIN_SLICE_MS. */ + int64_t wait_end = wait_ms < 0 ? INT64_MAX : ray_time_now_ms() + wait_ms; + int n = 0; + bool failed = false; + for (;;) { + bool std_ready = false; + for (uint32_t k = 0; k < nstd; k++) { + sevs[k] = stdin_events(poll->sels[sids[k]]->fd); + if (sevs[k]) std_ready = true; + } + + int remain = -1; + if (wait_end != INT64_MAX) { + int64_t r = wait_end - ray_time_now_ms(); + remain = r < 0 ? 0 : (r > INT_MAX ? INT_MAX : (int)r); + } + int slice = std_ready ? 0 : remain; + if (nstd && !std_ready && + (slice < 0 || slice > RAY_POLL_STDIN_SLICE_MS)) + slice = RAY_POLL_STDIN_SLICE_MS; + + if (nfds) { + n = WSAPoll(pfds, nfds, slice); + if (n == SOCKET_ERROR) { + if (WSAGetLastError() == WSAEINTR) continue; + failed = true; + break; + } + } else if (slice > 0) { + Sleep((DWORD)slice); + } else if (slice < 0) { + /* Only reachable with a live selector that is neither a + * socket nor stdin; nothing can wake us, so just yield. */ + Sleep(RAY_POLL_STDIN_SLICE_MS); + } + + if (n > 0 || std_ready) break; + if (remain == 0) break; /* deadline reached */ + if (!nstd && slice == remain) break; /* whole wait elapsed */ + } + + if (!failed) { + for (uint32_t j = 0; j < nfds && n > 0; j++) { + short re = pfds[j].revents; + if (!re) continue; + int ev = 0; + if (re & (POLLRDNORM | POLLRDBAND)) ev |= EV_IN; + if (re & POLLWRNORM) ev |= EV_OUT; + if (re & (POLLERR | POLLHUP | POLLNVAL)) ev |= EV_HUP; + /* The slot may have been reused by an earlier dispatch in + * this pass (deregister + accept): only act on the socket + * the event was reported for. */ + ray_selector_t* sel = pids[j] < poll->n_sels ? poll->sels[pids[j]] : NULL; + if (!sel || (SOCKET)sel->fd != pfds[j].fd) continue; + poll_dispatch(poll, pids[j], ev); + } + for (uint32_t k = 0; k < nstd; k++) { + if (!sevs[k]) continue; + ray_selector_t* sel = sids[k] < poll->n_sels ? poll->sels[sids[k]] : NULL; + if (!sel || sel->type != RAY_SEL_STDIN) continue; + poll_dispatch(poll, sids[k], sevs[k]); + } + } + + ray_sys_free(pfds); + ray_sys_free(pids); + ray_sys_free(sids); + ray_sys_free(sevs); + if (failed) return -1; + + if (poll->timers) { + if (ray_timers_fire_expired((ray_timers_t*)poll->timers) > 0) + ray_heap_note_activity(); + } + ray_heap_decay(); + if (bounded) break; + } + + return poll->code >= 0 ? poll->code : 0; } int64_t ray_poll_run(ray_poll_t* poll) @@ -72,4 +473,4 @@ int64_t ray_poll_run(ray_poll_t* poll) return ray_poll_run_for(poll, -1); } -#endif /* _WIN32 */ +#endif /* RAY_OS_WINDOWS */ From 7cb12aff93839c848f89eb30fc72a281f017304b Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Tue, 22 Sep 2026 14:58:56 +0300 Subject: [PATCH 03/13] fix(net): Winsock errno mapping, exclusive bind and IPC on Windows - mirror WSAGetLastError()/SO_ERROR into errno for send/recv/connect/ accept/bind/listen: callers branch on EAGAIN/ECONNREFUSED; - ipc_send_fn always overwrites errno, so a stale EAGAIN can no longer park a frame on a dead socket; - SO_EXCLUSIVEADDRUSE instead of SO_REUSEADDR (which lets a bind steal a port that is in use); - WSAStartup at load time; sock.h pulls in platform.h; - verbose IPC capture uses a temp file that works without admin rights; - the SIGURG out-of-band cancel stays POSIX-only. Co-Authored-By: Claude Opus 5 (1M context) --- src/core/ipc.c | 27 +++++++++++++++- src/core/sock.c | 84 ++++++++++++++++++++++++++++++++++++++++++++++--- src/core/sock.h | 2 +- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src/core/ipc.c b/src/core/ipc.c index 5143b5ce..c2593510 100644 --- a/src/core/ipc.c +++ b/src/core/ipc.c @@ -39,6 +39,7 @@ #ifdef RAY_OS_WINDOWS #define WIN32_LEAN_AND_MEAN + #include /* dup, dup2, close */ #include #include #else @@ -719,13 +720,31 @@ static ray_t* eval_payload_core(uint8_t* payload, size_t payload_len, * to fail the whole request because /tmp is full. The captured * string is then empty, and the response shape is still the 2-elem * list — clients can rely on that invariant. */ +#ifdef RAY_OS_WINDOWS +/* MSVCRT's tmpfile() creates its file in the drive root (admin-only on a + * stock system); use the user's temp directory, deleted on close ("D"). + * With no TMP/TEMP/USERPROFILE in the environment GetTempPath falls back + * to the (unwritable) Windows directory, so retry in the working dir. */ +static FILE* ipc_tmpfile(void) +{ + char dir[MAX_PATH + 1], path[MAX_PATH + 1]; + DWORD n = GetTempPathA(sizeof(dir), dir); + if (n == 0 || n > sizeof(dir) || !GetTempFileNameA(dir, "ray", 0, path)) { + if (!GetTempFileNameA(".", "ray", 0, path)) return NULL; + } + return fopen(path, "w+bD"); +} +#else +#define ipc_tmpfile() tmpfile() +#endif + static ray_t* eval_payload(uint8_t* payload, size_t payload_len, ray_ipc_header_t* hdr) { if (!(hdr->flags & RAY_IPC_FLAG_VERBOSE)) return eval_payload_core(payload, payload_len, hdr); - FILE* cap = tmpfile(); + FILE* cap = ipc_tmpfile(); int saved_out = -1, saved_err = -1; bool capturing = false; @@ -831,9 +850,13 @@ static int64_t ipc_send_fn(int64_t fd, uint8_t* buf, int64_t len) #ifdef RAY_OS_WINDOWS int n = send((ray_sock_t)fd, (const char*)buf, (int)len, 0); if (n < 0) { + /* Always overwrite errno: callers treat EAGAIN as "queue and retry", + * so a stale EAGAIN left over from an earlier call would turn a dead + * socket into a silently parked frame. */ int e = WSAGetLastError(); if (e == WSAEWOULDBLOCK) errno = EAGAIN; else if (e == WSAEINTR) errno = EINTR; + else errno = EIO; } return n; #else @@ -855,6 +878,7 @@ static int64_t ipc_send_fn(int64_t fd, uint8_t* buf, int64_t len) * ray_request_interrupt(). */ static volatile sig_atomic_t g_ipc_active_fd = -1; +#ifndef RAY_OS_WINDOWS /* no SIGURG on Windows: OOB cancel is POSIX-only */ static void ipc_sigurg_handler(int sig) { (void)sig; @@ -867,6 +891,7 @@ static void ipc_sigurg_handler(int sig) if (ray_sock_take_oob((ray_sock_t)fd)) ray_request_interrupt(); } +#endif static void ipc_install_oob_cancel(void) { diff --git a/src/core/sock.c b/src/core/sock.c index 750af93a..18f0c502 100644 --- a/src/core/sock.c +++ b/src/core/sock.c @@ -21,7 +21,7 @@ * SOFTWARE. */ -#ifndef RAY_OS_WINDOWS +#ifndef _WIN32 #define _GNU_SOURCE #endif @@ -48,6 +48,44 @@ /* ===== Socket Implementation ===== */ +#ifdef RAY_OS_WINDOWS +/* Winsock must be initialised before the process's first socket call; do it + * at load time so no entry point (listen, connect, a bare client) can miss + * it. Never paired with WSACleanup: process exit releases it. */ +__attribute__((constructor)) static void sock_win_startup(void) +{ + WSADATA wsa; + (void)WSAStartup(MAKEWORD(2, 2), &wsa); +} + +/* Winsock reports failures through WSAGetLastError(), never errno. The + * callers (poll loop, IPC) branch on errno == EINTR / EAGAIN, so mirror the + * Winsock code into errno after every failed socket call (or a code taken + * from SO_ERROR). */ +static void sock_win_errno_code(int code) +{ + switch (code) { + case WSAEWOULDBLOCK: errno = EAGAIN; break; + case WSAEINTR: errno = EINTR; break; + case WSAEINPROGRESS: errno = EINPROGRESS; break; + case WSAETIMEDOUT: errno = ETIMEDOUT; break; + case WSAECONNRESET: errno = ECONNRESET; break; + case WSAECONNABORTED: errno = ECONNABORTED; break; + case WSAECONNREFUSED: errno = ECONNREFUSED; break; + case WSAENOTCONN: errno = ENOTCONN; break; + case WSAENOTSOCK: errno = ENOTSOCK; break; + case WSAEADDRINUSE: errno = EADDRINUSE; break; + case WSAEINVAL: errno = EINVAL; break; + default: errno = EIO; break; + } +} + +static void sock_win_errno(void) +{ + sock_win_errno_code(WSAGetLastError()); +} +#endif + ray_sock_t ray_sock_listen_at(const char* host, uint16_t port) { /* NULL/empty host keeps the historical INADDR_ANY bind. A host that @@ -68,7 +106,15 @@ ray_sock_t ray_sock_listen_at(const char* host, uint16_t port) if (fd == RAY_INVALID_SOCK) return RAY_INVALID_SOCK; int yes = 1; +#ifdef RAY_OS_WINDOWS + /* Windows SO_REUSEADDR lets a second socket bind a port another one is + * actively listening on (the bind "steals" it). The POSIX meaning — + * rebind right after a restart, but fail while the port is in use — is + * SO_EXCLUSIVEADDRUSE here (TIME_WAIT never blocks a Windows bind). */ + setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (const char*)&yes, sizeof(yes)); +#else setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes)); +#endif struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); @@ -77,10 +123,16 @@ ray_sock_t ray_sock_listen_at(const char* host, uint16_t port) addr.sin_port = htons(port); if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { +#ifdef RAY_OS_WINDOWS + sock_win_errno(); /* e.g. EADDRINUSE for a taken port */ +#endif ray_sock_close(fd); return RAY_INVALID_SOCK; } if (listen(fd, 128) < 0) { +#ifdef RAY_OS_WINDOWS + sock_win_errno(); +#endif ray_sock_close(fd); return RAY_INVALID_SOCK; } @@ -97,6 +149,9 @@ ray_sock_t ray_sock_accept(ray_sock_t srv) ray_sock_t fd; do { fd = (ray_sock_t)accept(srv, NULL, NULL); +#ifdef RAY_OS_WINDOWS + if (fd == RAY_INVALID_SOCK) sock_win_errno(); +#endif } while (fd == RAY_INVALID_SOCK && errno == EINTR); if (fd == RAY_INVALID_SOCK) return RAY_INVALID_SOCK; @@ -115,8 +170,13 @@ ray_sock_t ray_sock_accept(ray_sock_t srv) static int sock_connect_one(ray_sock_t fd, const struct sockaddr* addr, socklen_t addrlen, int timeout_ms) { - if (timeout_ms <= 0) - return connect(fd, addr, addrlen) < 0 ? -1 : 0; + if (timeout_ms <= 0) { + if (connect(fd, addr, addrlen) == 0) return 0; +#ifdef RAY_OS_WINDOWS + sock_win_errno(); /* callers tell refusal from other failures by errno */ +#endif + return -1; + } ray_sock_set_nonblocking(fd); int rc = connect(fd, addr, addrlen); @@ -124,6 +184,7 @@ static int sock_connect_one(ray_sock_t fd, const struct sockaddr* addr, #ifdef RAY_OS_WINDOWS int werr = WSAGetLastError(); int in_progress = (werr == WSAEWOULDBLOCK || werr == WSAEINPROGRESS); + if (!in_progress) sock_win_errno_code(werr); #else int in_progress = (errno == EINPROGRESS); #endif @@ -136,13 +197,22 @@ static int sock_connect_one(ray_sock_t fd, const struct sockaddr* addr, do { pr = poll(&pfd, 1, timeout_ms); } while (pr < 0 && errno == EINTR); #endif if (pr == 0) { errno = ETIMEDOUT; return -1; } - if (pr < 0) return -1; + if (pr < 0) { +#ifdef RAY_OS_WINDOWS + sock_win_errno(); +#endif + return -1; + } /* Writable: harvest the pending connect result via SO_ERROR. */ int soerr = 0; socklen_t soerr_len = sizeof(soerr); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char*)&soerr, &soerr_len) < 0 || soerr != 0) { +#ifdef RAY_OS_WINDOWS + if (soerr != 0) sock_win_errno_code(soerr); /* a WSAE* code */ +#else if (soerr != 0) errno = soerr; +#endif return -1; } } @@ -213,6 +283,7 @@ int64_t ray_sock_send(ray_sock_t s, const void* buf, size_t len) while (rem > 0) { #ifdef RAY_OS_WINDOWS int n = send(s, (const char*)p, (int)rem, 0); + if (n < 0) sock_win_errno(); #else ssize_t n = send(s, p, rem, MSG_NOSIGNAL); #endif @@ -221,7 +292,11 @@ int64_t ray_sock_send(ray_sock_t s, const void* buf, size_t len) if (errno == EAGAIN || errno == EWOULDBLOCK) { /* Wait for write-readiness before retry */ struct pollfd pfd = { .fd = s, .events = POLLOUT }; +#ifdef RAY_OS_WINDOWS + WSAPoll(&pfd, 1, -1); +#else poll(&pfd, 1, -1); +#endif continue; } return -1; @@ -237,6 +312,7 @@ int64_t ray_sock_recv(ray_sock_t s, void* buf, size_t len) for (;;) { #ifdef RAY_OS_WINDOWS int n = recv(s, (char*)buf, (int)len, 0); + if (n < 0) sock_win_errno(); #else ssize_t n = recv(s, buf, len, 0); #endif diff --git a/src/core/sock.h b/src/core/sock.h index e35e566c..d400dbef 100644 --- a/src/core/sock.h +++ b/src/core/sock.h @@ -24,7 +24,7 @@ #ifndef RAY_SOCK_H #define RAY_SOCK_H -#include +#include "core/platform.h" /* ===== Socket Abstraction ===== */ From 4a392b761cba159a3f4507adf46a07ece85eee47 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Tue, 22 Sep 2026 14:58:56 +0300 Subject: [PATCH 04/13] fix: Windows platform layer (file mapping, pools, crash report, paths) - ray_vm_unmap_file only unmaps at a view's own base: UnmapViewOfFile drops the whole view for an interior pointer, which freed columns that carry a passenger index (munmap is a no-op there); - ray_vm_alloc_aligned returns its own allocation base, so pools are really released by ray_vm_free; - ray_vm_map_fd_ro maps for real (CSV reads always failed with io); - crash report via SetUnhandledExceptionFilter; - heap: file-backed spill stays POSIX-only (docs/architecture/memory.md); - domain: realpath substitute; symfile paths compare case/separator- insensitively so one file never gets two domains; - aof/csr/hnsw: platform handle for fsync, portable ray_mkdir. Co-Authored-By: Claude Opus 5 (1M context) --- src/core/crash.c | 107 +++++++++++++++++++++++++++++++++----------- src/core/platform.c | 59 ++++++++++++++++++------ src/io/csv.c | 4 +- src/mem/heap.c | 36 +++++++++++++-- src/store/aof.c | 13 +++++- src/store/csr.c | 3 +- src/store/fileio.h | 5 ++- src/store/hnsw.c | 3 +- src/table/domain.c | 42 +++++++++++++++-- 9 files changed, 218 insertions(+), 54 deletions(-) diff --git a/src/core/crash.c b/src/core/crash.c index 17d644ed..76f894b2 100644 --- a/src/core/crash.c +++ b/src/core/crash.c @@ -19,10 +19,13 @@ #include "core/crash.h" #include +#include #include #include -#if !defined(_WIN32) +#if defined(_WIN32) +#include +#else #include #endif @@ -42,7 +45,7 @@ static void cw(const char* s) { /* Write an unsigned value as 0x-prefixed hex. No libc formatting. * Routes through cw() so the write return value is handled (gcc's * warn_unused_result on write() is not silenced by a (void) cast). */ -static void cw_hex(unsigned long v) { +static void cw_hex(uint64_t v) { char buf[2 + 16 + 1]; static const char hexd[] = "0123456789abcdef"; buf[0] = '0'; buf[1] = 'x'; @@ -52,7 +55,8 @@ static void cw_hex(unsigned long v) { cw(buf); } -/* Write a small non-negative integer as decimal. */ +#if !defined(_WIN32) +/* Write a small non-negative integer as decimal (backtrace frame count). */ static void cw_int(int v) { if (v < 0) { cw("-"); v = -v; } char buf[16]; @@ -62,6 +66,75 @@ static void cw_int(int v) { while (v > 0 && i > 0) { buf[--i] = (char)('0' + v % 10); v /= 10; } cw(&buf[i]); } +#endif + +/* Banner precomputed at install time so the handler doesn't format it. */ +static char g_banner[128]; + +static void crash_banner_init(void) { + const char* v = +#ifdef RAYFORCE_VERSION + "rayforce " RAYFORCE_VERSION +#else + "rayforce" +#endif +#ifdef RAYFORCE_GIT_COMMIT + " (" RAYFORCE_GIT_COMMIT ")" +#endif + "\n"; + size_t vl = strlen(v); + if (vl >= sizeof(g_banner)) vl = sizeof(g_banner) - 1; + memcpy(g_banner, v, vl); + g_banner[vl] = '\0'; +} + +#if defined(_WIN32) + +/* ── Windows: structured exceptions ───────────────────────────────── */ + +static const char* exc_name(DWORD code) { + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: return "EXCEPTION_ACCESS_VIOLATION"; + case EXCEPTION_STACK_OVERFLOW: return "EXCEPTION_STACK_OVERFLOW"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "EXCEPTION_ILLEGAL_INSTRUCTION"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_IN_PAGE_ERROR: return "EXCEPTION_IN_PAGE_ERROR"; + default: return "exception"; + } +} + +/* Top-level filter: runs only for exceptions nobody else handled. Report + * and let the default handling (WER / exit with the exception code) run, + * which keeps the process exit status meaningful to the orchestrator. */ +static LONG WINAPI crash_filter(EXCEPTION_POINTERS* ep) { + const EXCEPTION_RECORD* er = ep ? ep->ExceptionRecord : NULL; + cw("\n=== rayforce fatal "); + cw(er ? exc_name(er->ExceptionCode) : "exception"); + if (er) { + cw(" code "); cw_hex((uint64_t)er->ExceptionCode); + cw(" at "); cw_hex((uint64_t)(uintptr_t)er->ExceptionAddress); + if (er->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && + er->NumberParameters >= 2) { + cw(" fault addr "); + cw_hex((uint64_t)er->ExceptionInformation[1]); + } + } + cw(" ===\n"); + cw(g_banner); + return EXCEPTION_CONTINUE_SEARCH; +} + +void ray_crash_install(void) { + crash_banner_init(); + /* Reserve stack for the filter itself, so a stack-overflow exception + * can still be reported (the Windows analogue of sigaltstack). */ + ULONG guarantee = 64 * 1024; + (void)SetThreadStackGuarantee(&guarantee); + SetUnhandledExceptionFilter(crash_filter); +} + +#else /* POSIX */ static const char* sig_name(int sig) { switch (sig) { @@ -74,9 +147,6 @@ static const char* sig_name(int sig) { } } -/* Banner precomputed at install time so the handler doesn't format it. */ -static char g_banner[128]; - /* ── the handler ──────────────────────────────────────────────────── */ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { @@ -84,11 +154,10 @@ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { cw("\n=== rayforce fatal "); cw(sig_name(sig)); - if (info) { cw(" at fault addr "); cw_hex((unsigned long)info->si_addr); } + if (info) { cw(" at fault addr "); cw_hex((uint64_t)(uintptr_t)info->si_addr); } cw(" ===\n"); cw(g_banner); -#if !defined(_WIN32) void* frames[64]; int n = backtrace(frames, 64); /* backtrace_symbols_fd writes directly to the fd without allocating. */ @@ -96,7 +165,6 @@ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { cw("=== end backtrace ("); cw_int(n); cw(" frames) ===\n"); -#endif /* Restore the default disposition and re-raise, so the process dies * from the original signal: this preserves the core dump and reports @@ -116,24 +184,8 @@ static void crash_handler(int sig, siginfo_t* info, void* ucontext) { static char g_altstack[RAY_CRASH_ALTSTACK_SZ]; void ray_crash_install(void) { -#if !defined(_WIN32) /* Precompute the version banner once (async-signal-safe reuse). */ - { - const char* v = -#ifdef RAYFORCE_VERSION - "rayforce " RAYFORCE_VERSION -#else - "rayforce" -#endif -#ifdef RAYFORCE_GIT_COMMIT - " (" RAYFORCE_GIT_COMMIT ")" -#endif - "\n"; - size_t vl = strlen(v); - if (vl >= sizeof(g_banner)) vl = sizeof(g_banner) - 1; - memcpy(g_banner, v, vl); - g_banner[vl] = '\0'; - } + crash_banner_init(); /* Warm up the unwinder: the first backtrace() may dlopen libgcc and * allocate, which must not happen inside the handler. */ @@ -157,5 +209,6 @@ void ray_crash_install(void) { static const int sigs[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT }; for (size_t i = 0; i < sizeof(sigs) / sizeof(sigs[0]); i++) (void)sigaction(sigs[i], &sa, NULL); -#endif } + +#endif /* _WIN32 */ diff --git a/src/core/platform.c b/src/core/platform.c index 89852918..bad99c91 100644 --- a/src/core/platform.c +++ b/src/core/platform.c @@ -469,6 +469,7 @@ void ray_sem_signal(ray_sem_t* s) { #define WIN32_LEAN_AND_MEAN #endif #include +#include /* _get_osfhandle */ #include "mem/sys.h" /* -------------------------------------------------------------------------- @@ -518,13 +519,34 @@ void* ray_vm_map_file(const char* path, size_t* out_size) { void ray_vm_unmap_file(void* ptr, size_t size) { if (!ptr) return; - UnmapViewOfFile(ptr); + /* munmap releases exactly [ptr, ptr+size) and is a no-op (EINVAL) for an + * unaligned ptr — the heap relies on that when it frees a block living + * inside a column's mapping (a passenger index). UnmapViewOfFile instead + * drops the WHOLE view containing ptr, which would pull the column out + * from under its live references. So only unmap at the view's own base; + * an interior block goes away with the view. The byte accounting still + * follows the call, exactly as on POSIX. */ + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(ptr, &mbi, sizeof(mbi)) && mbi.AllocationBase == ptr) + UnmapViewOfFile(ptr); ray_sys_track_file_sub((int64_t)size); } -/* Windows never reaches the fd/mmap CSV path (#ifndef RAY_OS_WINDOWS), so this - * is an unused stub kept only for API completeness. */ -void* ray_vm_map_fd_ro(int fd, size_t size) { (void)fd; (void)size; return NULL; } +/* Read-only view of an open CRT descriptor (the CSV reader maps the file this + * way, then closes the fd). The view keeps the file alive on its own, so both + * the mapping handle and the caller's fd may be closed afterwards. */ +void* ray_vm_map_fd_ro(int fd, size_t size) { + if (size == 0) return NULL; + HANDLE hFile = (HANDLE)_get_osfhandle(fd); + if (hFile == INVALID_HANDLE_VALUE) return NULL; + HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + if (!hMap) return NULL; + void* p = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, size); + CloseHandle(hMap); + if (!p) return NULL; + ray_sys_track_file_add((int64_t)size); + return p; +} void ray_vm_advise_seq(void* ptr, size_t size) { /* PrefetchVirtualMemory is Win8.1+. Best-effort; ignore failure. */ @@ -546,16 +568,25 @@ void ray_vm_release_block(void* blk, size_t bsize, bool hugepage) { } void* ray_vm_alloc_aligned(size_t size, size_t alignment) { - /* Over-allocate, find aligned offset. Can't trim on Windows, so the - * pool header's vm_base field stores the original base for VirtualFree. */ - void* mem = VirtualAlloc(NULL, size + alignment, - MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - if (!mem) return NULL; - uintptr_t aligned = ((uintptr_t)mem + alignment - 1) & ~(alignment - 1); - /* Count the kept `size` to balance ray_vm_free(ptr, size); the alignment - * slack Windows cannot trim is left uncounted (parity with POSIX). */ - ray_sys_track_add((int64_t)size); - return (void*)aligned; + /* VirtualFree(MEM_RELEASE) only accepts the exact base VirtualAlloc + * returned, and a reservation cannot be trimmed. So find an aligned + * hole by reserving size+alignment, release it, and allocate exactly + * `size` at the aligned address inside it. Another thread may take the + * hole in between; retry a few times. The result is its own allocation + * base, so ray_vm_free(ptr, size) releases it like any other block. */ + for (int attempt = 0; attempt < 16; attempt++) { + void* probe = VirtualAlloc(NULL, size + alignment, MEM_RESERVE, PAGE_NOACCESS); + if (!probe) return NULL; + uintptr_t aligned = ((uintptr_t)probe + alignment - 1) & ~(alignment - 1); + VirtualFree(probe, 0, MEM_RELEASE); + void* p = VirtualAlloc((void*)aligned, size, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (p) { + ray_sys_track_add((int64_t)size); + return p; + } + } + return NULL; } bool ray_vm_hugepage(void* ptr, size_t size) { (void)ptr; (void)size; return false; } diff --git a/src/io/csv.c b/src/io/csv.c index 269fb3da..25302c1f 100644 --- a/src/io/csv.c +++ b/src/io/csv.c @@ -65,8 +65,8 @@ #include #ifndef RAY_OS_WINDOWS #include -#endif #include +#endif /* -------------------------------------------------------------------------- * Constants @@ -104,7 +104,9 @@ static inline uint64_t csv_prog_at(size_t file_size, unsigned pct) { * mmap flags * -------------------------------------------------------------------------- */ +#ifndef RAY_OS_WINDOWS #define MMAP_FLAGS MAP_PRIVATE +#endif /* -------------------------------------------------------------------------- * Scratch memory helpers (same pattern as exec.c). diff --git a/src/mem/heap.c b/src/mem/heap.c index 1f82a7e6..906ef225 100644 --- a/src/mem/heap.c +++ b/src/mem/heap.c @@ -44,11 +44,21 @@ #include /* getpid, close, ftruncate, unlink */ #include /* open, fcntl, F_PREALLOCATE on macOS */ #include -#include /* mmap, munmap */ #include /* O_* modes */ #include #include +/* File-backed spill (a pool or direct block mapped over a preallocated temp + * file when the anonymous mapping is refused) is POSIX-only. Windows pools + * take only the anonymous VirtualAlloc path (docs/architecture/memory.md): + * the spill helpers are compiled out and swap_fd stays -1 there. */ +#if defined(RAY_OS_WINDOWS) +# define RAY_HEAP_FILE_SPILL 0 +#else +# define RAY_HEAP_FILE_SPILL 1 +# include /* mmap, munmap */ +#endif + #ifdef DEBUG /* ===================================================================== * Debug-only stale-pointer detector (issue #240 investigation). @@ -64,7 +74,9 @@ * choice for chasing double releases (found while debugging #240). * RAY_DFD_NO_ABORT=1 reports without aborting. * ===================================================================== */ +#if !defined(RAY_OS_WINDOWS) #include +#endif #define DFD_CAP_BITS 20 #define DFD_CAP (1u << DFD_CAP_BITS) @@ -149,10 +161,12 @@ static void dfd_purge_range(uintptr_t lo, uintptr_t hi) { } static void dfd_report(const char* who, const void* p) { + fprintf(stderr, "\n=== DFD: %s on FREED block %p ===\n", who, p); +#if !defined(RAY_OS_WINDOWS) void* frames[64]; int n = backtrace(frames, 64); - fprintf(stderr, "\n=== DFD: %s on FREED block %p ===\n", who, p); backtrace_symbols_fd(frames, n, 2); +#endif fflush(stderr); if (!getenv("RAY_DFD_NO_ABORT")) abort(); } @@ -179,6 +193,7 @@ static void dfd_validate_freelists(void); * contiguous first, fall back to non-contiguous, then ftruncate to * extend the file size if needed (F_PREALLOCATE doesn't grow the file * beyond its current size). */ +#if RAY_HEAP_FILE_SPILL static int heap_preallocate(int fd, off_t offset, off_t len) { #if defined(__APPLE__) fstore_t fs = { @@ -202,6 +217,7 @@ static int heap_preallocate(int fd, off_t offset, off_t len) { return posix_fallocate(fd, offset, len); #endif } +#endif /* RAY_HEAP_FILE_SPILL */ /* -------------------------------------------------------------------------- * Static asserts @@ -574,6 +590,9 @@ static bool heap_add_pool(ray_heap_t* h, uint8_t order) { if (!heap_anon_would_exceed(pool_size)) mem = ray_vm_alloc_aligned(pool_size, pool_size); +#if !RAY_HEAP_FILE_SPILL + if (!mem) return false; +#else if (!mem) { /* Anonymous mmap refused — usually means RAM+swap can't satisfy * pool_size right now. Fall back to file-backed mmap: create a @@ -668,6 +687,7 @@ static bool heap_add_pool(ray_heap_t* h, uint8_t order) { ray_sys_free(swap_path); swap_path = NULL; } +#endif /* RAY_HEAP_FILE_SPILL */ /* Enable transparent huge pages on anon pools (Linux). Self-aligned * 32MB pools are 2MB-aligned, hence THP-eligible. Never on file-backed @@ -1184,6 +1204,10 @@ static void ray_detach_owned_refs(ray_t* v) { * failure. */ static void* heap_direct_map_file(ray_heap_t* h, size_t map_size, int* out_fd, char** out_path) { +#if !RAY_HEAP_FILE_SPILL + (void)h; (void)map_size; (void)out_fd; (void)out_path; + return NULL; +#else static _Atomic uint64_t direct_swap_counter = 0; uint64_t cnt = atomic_fetch_add_explicit(&direct_swap_counter, 1, memory_order_relaxed); @@ -1213,6 +1237,7 @@ static void* heap_direct_map_file(ray_heap_t* h, size_t map_size, *out_fd = fd; *out_path = path; return mapped; +#endif /* RAY_HEAP_FILE_SPILL */ } /* -------------------------------------------------------------------------- @@ -1713,6 +1738,7 @@ void ray_free(ray_t* v) { if (h) RAY_STAT(h->stats.free_count++); atomic_fetch_sub_explicit(&g_direct_bytes, (int64_t)map_size, memory_order_relaxed); atomic_fetch_sub_explicit(&g_direct_count, 1, memory_order_relaxed); +#if RAY_HEAP_FILE_SPILL if (swap_fd >= 0) { /* File-backed spill: mapped directly (not via ray_vm_alloc), so * unmap + uncount by hand, then close and unlink the spill file. */ @@ -1720,7 +1746,11 @@ void ray_free(ray_t* v) { ray_sys_track_sub((int64_t)map_size); close(swap_fd); if (swap_path) { unlink(swap_path); ray_sys_free(swap_path); } - } else if (direct_cache_put(base, map_size)) { + } else +#else + (void)swap_fd; (void)swap_path; +#endif + if (direct_cache_put(base, map_size)) { /* Stashed for reuse: pages stay resident, so the block keeps * its committed-RAM and watermark accounting; only the live * stats above dropped. Eviction (direct_cache_drain) performs diff --git a/src/store/aof.c b/src/store/aof.c index fe01b480..e85831ae 100644 --- a/src/store/aof.c +++ b/src/store/aof.c @@ -49,6 +49,15 @@ #include #include +/* ray_file_sync takes the platform handle: the descriptor itself on POSIX, + * the underlying HANDLE on Windows. */ +#ifdef RAY_OS_WINDOWS +#include +#define AOF_FP_HANDLE(fp) ((ray_fd_t)_get_osfhandle(fileno(fp))) +#else +#define AOF_FP_HANDLE(fp) ((ray_fd_t)fileno(fp)) +#endif + #define AOF_PATH_MAX 1024 /* Segment-path buffers are sized past the worst case (dir + '/' + 24-char * segment name) so gcc's -Wformat-truncation can prove snprintf fits even @@ -390,7 +399,7 @@ static ray_err_t aof_rotate(ray_aof_t* log) { if (err != RAY_OK) return err; } if (fflush(log->fp) != 0) return RAY_ERR_IO; - if (ray_file_sync((ray_fd_t)fileno(log->fp)) != RAY_OK) return RAY_ERR_IO; + if (ray_file_sync(AOF_FP_HANDLE(log->fp)) != RAY_OK) return RAY_ERR_IO; if (fclose(log->fp) != 0) { log->fp = NULL; return RAY_ERR_IO; } char path[AOF_SEGPATH_MAX]; @@ -442,7 +451,7 @@ ray_err_t ray_aof_commit(ray_aof_t* log) { ray_err_t err = aof_write_frame(log); if (err != RAY_OK) return err; if (fflush(log->fp) != 0) return RAY_ERR_IO; - return ray_file_sync((ray_fd_t)fileno(log->fp)); + return ray_file_sync(AOF_FP_HANDLE(log->fp)); } int64_t ray_aof_next_lsn(const ray_aof_t* log) { diff --git a/src/store/csr.c b/src/store/csr.c index 59f43b5a..b042fb51 100644 --- a/src/store/csr.c +++ b/src/store/csr.c @@ -24,6 +24,7 @@ #include "csr.h" #include "store/col.h" #include "mem/sys.h" +#include "store/fileio.h" /* ray_mkdir */ #include #include #include @@ -464,7 +465,7 @@ ray_err_t ray_rel_save(ray_rel_t* rel, const char* dir) { if (!rel || !dir) return RAY_ERR_IO; /* Create directory */ - if (mkdir(dir, 0755) != 0 && errno != EEXIST) return RAY_ERR_IO; + if (ray_mkdir(dir) != RAY_OK) return RAY_ERR_IO; ray_err_t err = csr_save(&rel->fwd, dir, "fwd"); if (err != RAY_OK) return err; diff --git a/src/store/fileio.h b/src/store/fileio.h index 658e5606..c6f954c8 100644 --- a/src/store/fileio.h +++ b/src/store/fileio.h @@ -24,10 +24,13 @@ #ifndef RAY_FILEIO_H #define RAY_FILEIO_H -#include +#include "core/platform.h" /* Cross-platform file I/O (locking, sync, atomic rename) */ #ifdef RAY_OS_WINDOWS + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN /* keep / macros out */ + #endif #include typedef HANDLE ray_fd_t; #define RAY_FD_INVALID INVALID_HANDLE_VALUE diff --git a/src/store/hnsw.c b/src/store/hnsw.c index 339119a1..a350c433 100644 --- a/src/store/hnsw.c +++ b/src/store/hnsw.c @@ -23,6 +23,7 @@ #include "hnsw.h" #include "mem/sys.h" +#include "store/fileio.h" /* ray_mkdir */ #include #include #include @@ -818,7 +819,7 @@ typedef struct { ray_err_t ray_hnsw_save(const ray_hnsw_t* idx, const char* dir) { if (!idx || !dir) return RAY_ERR_IO; - if (mkdir(dir, 0755) != 0 && errno != EEXIST) return RAY_ERR_IO; + if (ray_mkdir(dir) != RAY_OK) return RAY_ERR_IO; char path[1024]; FILE* f; diff --git a/src/table/domain.c b/src/table/domain.c index ba87c492..0e9f5869 100644 --- a/src/table/domain.c +++ b/src/table/domain.c @@ -210,6 +210,40 @@ static inline void dom_unlock(void) { /* ---- FILE domain construction / destruction ------------------------------- */ +/* realpath(3) contract: absolute path of an EXISTING file, else NULL. + * Windows has no realpath; _fullpath only normalizes (it succeeds for + * missing files too), so existence is checked separately. */ +static char* dom_realpath(const char* path, char resolved[PATH_MAX]) { +#if defined(RAY_OS_WINDOWS) + if (!_fullpath(resolved, path, PATH_MAX)) return NULL; + if (GetFileAttributesA(resolved) == INVALID_FILE_ATTRIBUTES) return NULL; + return resolved; +#else + return realpath(path, resolved); +#endif +} + +/* Do two resolved paths name the same file? On Windows one file is + * reachable as C:\db\sym, C:\db/sym or C:\DB\Sym (NTFS is case-insensitive + * and _fullpath keeps the caller's separators and case), so compare with + * '\\' == '/' and ASCII case folded — a plain strcmp would key one symfile + * twice and open two diverging domains for it. The paths themselves are + * left as given: they are also used to create the file. */ +static bool dom_path_eq(const char* a, const char* b) { +#if defined(RAY_OS_WINDOWS) + for (;; a++, b++) { + char ca = *a == '\\' ? '/' : *a; + char cb = *b == '\\' ? '/' : *b; + if (ca >= 'A' && ca <= 'Z') ca = (char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (char)(cb - 'A' + 'a'); + if (ca != cb) return false; + if (!ca) return true; + } +#else + return strcmp(a, b) == 0; +#endif +} + /* Resolved cache key for `path`. realpath of the file when it exists; * for to-be-created symfiles, realpath of the parent + "/" + basename * (the parent must exist). malloc'd. */ @@ -218,7 +252,7 @@ static char* dom_resolve_path(const char* path) { * realpath(NULL)/strdup's libc-malloc'd buffers, so the returned key is * uniformly buddy-allocated and the caller releases it with ray_free_raw. */ char resolved[PATH_MAX]; - if (realpath(path, resolved)) { + if (dom_realpath(path, resolved)) { size_t n = strlen(resolved); char* out = (char*)ray_sys_alloc(n + 1); if (out) memcpy(out, resolved, n + 1); @@ -238,7 +272,7 @@ static char* dom_resolve_path(const char* path) { memcpy(tmp, path, plen + 1); char* dir = dirname(tmp); char rdir[PATH_MAX]; - if (!realpath(dir, rdir)) return NULL; + if (!dom_realpath(dir, rdir)) return NULL; size_t dlen = strlen(rdir); char* out = (char*)ray_sys_alloc(dlen + 1 + blen + 1); if (!out) return NULL; @@ -519,7 +553,7 @@ static ray_sym_domain_t* dom_open_impl(const char* path, bool create) { dom_lock(); for (ray_sym_domain_t* d = g_domains; d; d = d->next) { - if (strcmp(d->path, rpath) == 0) { + if (dom_path_eq(d->path, rpath)) { /* Revalidate: external append-only growth extends in place; * any other divergence is loud (NULL). */ size_t cur_size = exists ? (size_t)st.st_size : 0; @@ -591,7 +625,7 @@ static ray_sym_domain_t* dom_open_impl(const char* path, bool create) { * the winner (pointer equality must hold for one resolved path). */ dom_lock(); for (ray_sym_domain_t* e = g_domains; e; e = e->next) { - if (strcmp(e->path, d->path) == 0) { + if (dom_path_eq(e->path, d->path)) { e->rc++; dom_unlock(); dom_destroy(d); From 9f5bbaf9995cbf9f771e3a652d0a7ec0d8a61bf3 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Tue, 22 Sep 2026 14:58:56 +0300 Subject: [PATCH 05/13] fix: REPL, profiler and system builtins build and work on Windows - term.h/profile.h include platform.h and a lean ; - term_write for the Windows console, errno.h and core count in the REPL; - .sys.info reports page-size and total-mem on Windows too; - KEY_READ no longer collides with . Co-Authored-By: Claude Opus 5 (1M context) --- src/app/repl.c | 6 +++++- src/app/term.c | 7 ++++++- src/app/term.h | 5 ++++- src/core/profile.h | 3 +++ src/ops/query.c | 4 +++- src/ops/system.c | 16 +++++++++++++++- 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/app/repl.c b/src/app/repl.c index 6775c890..7036c63c 100644 --- a/src/app/repl.c +++ b/src/app/repl.c @@ -51,6 +51,7 @@ #include #include #include +#include #if defined(RAY_OS_WINDOWS) #include @@ -59,7 +60,6 @@ #define STDIN_FD 0 #else #include -#include #include #define STDIN_FD STDIN_FILENO #endif @@ -385,7 +385,11 @@ static void print_banner(void) { char cpu[256]; get_cpu_name(cpu, sizeof(cpu)); int64_t mem_mb = get_total_mem_mb(); +#if defined(RAY_OS_WINDOWS) + int ncores = (int)ray_thread_count(); +#else int ncores = (int)sysconf(_SC_NPROCESSORS_ONLN); +#endif /* "Using" count reflects the actual worker-pool size, not ncores. * ray_pool_get() is a lazy initializer — callers might not have diff --git a/src/app/term.c b/src/app/term.c index ffae9548..79509f18 100644 --- a/src/app/term.c +++ b/src/app/term.c @@ -67,7 +67,12 @@ typedef struct stat hist_stat_t; #define RAY_BLOCK_FROM_DATA(ptr) ((ray_t*)((char*)(ptr) - sizeof(ray_t))) /* Suppress -Wunused-result for terminal I/O writes to stdout. */ -#if !defined(RAY_OS_WINDOWS) +#if defined(RAY_OS_WINDOWS) +static inline void term_write(const void* buf, size_t len) { + int r = _write(1, buf, (unsigned)len); + (void)r; +} +#else static inline void term_write(const void* buf, size_t len) { ssize_t r = write(STDOUT_FILENO, buf, len); (void)r; diff --git a/src/app/term.h b/src/app/term.h index cf89457b..8c7f23fa 100644 --- a/src/app/term.h +++ b/src/app/term.h @@ -24,9 +24,12 @@ #ifndef RAY_TERM_H #define RAY_TERM_H -#include +#include "core/platform.h" #if defined(RAY_OS_WINDOWS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN /* keep / macros out */ +#endif #include #define KEYCODE_RETURN '\r' #else diff --git a/src/core/profile.h b/src/core/profile.h index 02a71ab6..cc5ab6ad 100644 --- a/src/core/profile.h +++ b/src/core/profile.h @@ -28,6 +28,9 @@ #include #if defined(RAY_OS_WINDOWS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN /* keep / macros out */ +#endif #include #else #include diff --git a/src/ops/query.c b/src/ops/query.c index 2008af99..056d2388 100644 --- a/src/ops/query.c +++ b/src/ops/query.c @@ -11017,7 +11017,9 @@ ray_t* ray_select(ray_t** args, int64_t n) { /* Type-aware key element reader. Normalizes any * comparable scalar key into an int64_t so linear * scans can use equality. For floats we bitcast so - * NaN and -0/+0 match the DAG's hash-equality. */ + * NaN and -0/+0 match the DAG's hash-equality. + * (via ) owns the name on Windows. */ + #undef KEY_READ #define KEY_READ(dst, vec, base_type, idx) do { \ const void* _d = ray_data(vec); \ switch (base_type) { \ diff --git a/src/ops/system.c b/src/ops/system.c index 17641564..6d5a55cc 100644 --- a/src/ops/system.c +++ b/src/ops/system.c @@ -65,6 +65,7 @@ void* ray_runtime_get_sys_args(void); #define RAY_POPEN(c, m) popen((c), (m)) #define RAY_PCLOSE(f) pclose(f) #else +#include /* access, F_OK */ #define RAY_POPEN(c, m) _popen((c), (m)) #define RAY_PCLOSE(f) _pclose(f) #endif @@ -1532,10 +1533,23 @@ ray_t* ray_sysinfo_fn(ray_t** args, int64_t n) { ray_t* v3 = make_i64(ray_sys_total_ram()); vals = ray_list_append(vals, v3); ray_release(v3); #else + SYSTEM_INFO si; + GetSystemInfo(&si); + int64_t s1 = ray_sym_intern("cores", 5); keys = ray_vec_append(keys, &s1); - ray_t* v1 = make_i64(1); + ray_t* v1 = make_i64((int64_t)si.dwNumberOfProcessors); vals = ray_list_append(vals, v1); ray_release(v1); + + int64_t s2 = ray_sym_intern("page-size", 9); + keys = ray_vec_append(keys, &s2); + ray_t* v2 = make_i64((int64_t)si.dwPageSize); + vals = ray_list_append(vals, v2); ray_release(v2); + + int64_t s3 = ray_sym_intern("total-mem", 9); + keys = ray_vec_append(keys, &s3); + ray_t* v3 = make_i64(ray_sys_total_ram()); + vals = ray_list_append(vals, v3); ray_release(v3); #endif /* Process and host identity (#573). An embedded process previously had From 0807e1cb7b2f4fb4a7681a961c1904f2ab2cf4dd Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Tue, 22 Sep 2026 14:58:57 +0300 Subject: [PATCH 06/13] test: run the suite on Windows - runner: ';; @requires: posix' marks .rfl files whose fixtures or checks need a POSIX shell/filesystem; on Windows they are reported as SKIP; - shell-free ray_test_rm_rf / ray_test_mkdir_p replace system("rm -rf") and "mkdir -p" in C tests; test.h maps the few POSIX helpers tests use; - tests of POSIX-only behaviour (setrlimit, ENOTDIR, read-only dirs, AF_UNIX, file spill, Winsock send buffering) skip with the reason; - fixture fixes that were latent on any platform: binary-mode CSV fixtures, a per-test AOF dir, journal closed before the crash rename. Co-Authored-By: Claude Opus 5 (1M context) --- test/ipc_harness.h | 5 + test/main.c | 62 ++++++++++++ test/rfl/collection/atomic_map_coverage.rfl | 1 + test/rfl/collection/collection_branch_cov.rfl | 1 + test/rfl/collection/cov3.rfl | 1 + test/rfl/collection/cov4.rfl | 1 + test/rfl/collection/cov5.rfl | 1 + test/rfl/collection/cov6.rfl | 1 + test/rfl/group/group_key_types.rfl | 1 + test/rfl/io/csv_branch_cov.rfl | 1 + test/rfl/io/csv_parallel_scan.rfl | 1 + test/rfl/io/csv_rayfall_temporal.rfl | 1 + test/rfl/io/csv_types.rfl | 1 + test/rfl/io/read_until_eof.rfl | 1 + test/rfl/journal/ops_journal.rfl | 1 + test/rfl/journal/ops_journal_purge.rfl | 1 + test/rfl/lang/guid_entropy.rfl | 1 + test/rfl/lang/parse_branch_cov.rfl | 1 + test/rfl/linkop/coverage.rfl | 1 + test/rfl/null/slice_has_nulls.rfl | 1 + test/rfl/null/sort_null_placement.rfl | 1 + test/rfl/null/sym_str_null_compare.rfl | 1 + test/rfl/ops/internal_coverage.rfl | 1 + test/rfl/query/update_by_sym_width.rfl | 1 + test/rfl/regress/load_select_isnull_mask.rfl | 1 + test/rfl/sort/sort_coverage2.rfl | 1 + test/rfl/storage/shared_sym_domain.rfl | 1 + test/rfl/storage/splay_coverage.rfl | 1 + test/rfl/store/col_format_generation.rfl | 1 + test/rfl/store/indexed_col_aux_refs.rfl | 1 + test/rfl/strop/like_seen_proj.rfl | 1 + test/rfl/strop/string_branch_cov.rfl | 1 + test/rfl/symbol/sym_coverage.rfl | 1 + test/rfl/system/cli_flag_values.rfl | 1 + test/rfl/system/csv_auto_int_width.rfl | 1 + .../rfl/system/csv_explicit_numeric_types.rfl | 1 + test/rfl/system/db_get.rfl | 1 + test/rfl/system/db_parted_fill.rfl | 1 + test/rfl/system/db_sym_resolution.rfl | 1 + test/rfl/system/ipc_diff.rfl | 1 + test/rfl/system/ipc_first_last.rfl | 1 + test/rfl/system/ipc_open_errors.rfl | 1 + test/rfl/system/ipc_open_timeout.rfl | 1 + test/rfl/system/listen_fatal.rfl | 1 + test/rfl/system/load_errors.rfl | 1 + test/rfl/system/load_home.rfl | 1 + test/rfl/system/log_journal.rfl | 1 + test/rfl/system/log_journal_advanced.rfl | 1 + test/rfl/system/os_fs.rfl | 1 + test/rfl/system/part.rfl | 1 + test/rfl/system/part_branch_cov.rfl | 1 + test/rfl/system/piped_timers.rfl | 1 + test/rfl/system/process_identity.rfl | 1 + test/rfl/system/querylog_ipc.rfl | 1 + test/rfl/system/read_csv.rfl | 1 + test/rfl/system/startup_script_fatal.rfl | 1 + test/rfl/system/system_branch_cov.rfl | 1 + test/rfl/system/timer_overrun.rfl | 1 + test/stress_store.c | 36 +++++-- test/test.h | 54 +++++++++++ test/test_aof.c | 7 +- test/test_csv.c | 70 ++++++------- test/test_heap.c | 31 ++++++ test/test_ipc.c | 97 +++++++++++++------ test/test_journal.c | 64 +++++++++++- test/test_link.c | 24 +++-- test/test_mcast.c | 30 +++--- test/test_repl.c | 6 +- test/test_runtime.c | 14 ++- test/test_splay.c | 43 ++++---- test/test_store.c | 53 +++++----- test/test_traverse.c | 8 +- 72 files changed, 496 insertions(+), 164 deletions(-) diff --git a/test/ipc_harness.h b/test/ipc_harness.h index 5f9066dd..8dad45ae 100644 --- a/test/ipc_harness.h +++ b/test/ipc_harness.h @@ -48,8 +48,13 @@ #include "core/runtime.h" #include "mem/sys.h" #include "lang/internal.h" +#ifdef RAY_OS_WINDOWS +#include +#include +#else #include #include +#endif #include #include diff --git a/test/main.c b/test/main.c index 6cf59894..42426e6a 100644 --- a/test/main.c +++ b/test/main.c @@ -50,6 +50,7 @@ #include "lang/format.h" #include "ops/internal.h" #include "ops/idxop.h" +#include "store/fileio.h" /* ray_mkdir_p — ray_test_mkdir_p */ /* __RUNTIME is internal test plumbing; runtime API declarations come from * . */ @@ -376,6 +377,16 @@ static test_result_t run_rfl_file(const char* path) { src[r] = '\0'; fclose(f); + /* ";; @requires: posix" anywhere in a file marks it as depending on a + * POSIX shell / filesystem (.sys.exec pipelines, /proc). Where that + * does not exist the file is reported as SKIP, never silently dropped. */ +#if defined(_WIN32) + if (strstr(src, ";; @requires: posix")) { + free(src); + SKIP("requires POSIX shell/filesystem"); + } +#endif + int line_no = 0; int assert_count = 0; /* tallies LHS -- RHS and EXPR !- SUBSTR lines */ char* p = src; @@ -848,7 +859,58 @@ static int name_matches_filter(const char* name, const char* filter) { return strstr(name, filter) != NULL; } +/* ---- Shell-free filesystem helpers (declared in test.h) ---- */ + +int ray_test_rm_rf(const char* path) { + struct stat st; + if (lstat(path, &st) != 0) return 0; /* already gone */ + if (S_ISDIR(st.st_mode)) { + DIR* d = opendir(path); + if (d) { + struct dirent* ent; + char child[4096]; + while ((ent = readdir(d)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) + continue; + snprintf(child, sizeof(child), "%s/%s", path, ent->d_name); + ray_test_rm_rf(child); + } + closedir(d); + } + return rmdir(path); + } + return unlink(path); +} + +int ray_test_mkdir_p(const char* path) { + return ray_mkdir_p(path) == RAY_OK ? 0 : -1; +} + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +long ray_test_sysconf(int name) { + SYSTEM_INFO si; + GetSystemInfo(&si); + switch (name) { + case _SC_PAGESIZE: return (long)si.dwPageSize; + case _SC_NPROCESSORS_ONLN: return (long)si.dwNumberOfProcessors; + case _SC_PHYS_PAGES: { + MEMORYSTATUSEX ms; + ms.dwLength = sizeof(ms); + if (!GlobalMemoryStatusEx(&ms)) return -1; + return (long)(ms.ullTotalPhys / si.dwPageSize); + } + default: errno = EINVAL; return -1; + } +} +#endif + int main(int argc, char** argv) { +#if defined(_WIN32) + (void)_mkdir("/tmp"); /* tests use "/tmp/..." paths (see test.h) */ +#endif ray_expr_stats_init(); ray_idx_stats_init(); g_color = isatty(fileno(stdout)); diff --git a/test/rfl/collection/atomic_map_coverage.rfl b/test/rfl/collection/atomic_map_coverage.rfl index 7eba0de7..28c7e7b2 100644 --- a/test/rfl/collection/atomic_map_coverage.rfl +++ b/test/rfl/collection/atomic_map_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; atomic_map_binary_op / atomic_map_unary coverage. ;; Exercises boxed-list paths, empty collections, nested auto-map, ;; recursive map, and error propagation. diff --git a/test/rfl/collection/collection_branch_cov.rfl b/test/rfl/collection/collection_branch_cov.rfl index b187b9e0..dc0e28bb 100644 --- a/test/rfl/collection/collection_branch_cov.rfl +++ b/test/rfl/collection/collection_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; collection_branch_cov.rfl — branch coverage for src/ops/collection.c ;; ;; Targets uncovered branches identified at 64.84% baseline. Each section diff --git a/test/rfl/collection/cov3.rfl b/test/rfl/collection/cov3.rfl index 0c2e08f5..dc23bea6 100644 --- a/test/rfl/collection/cov3.rfl +++ b/test/rfl/collection/cov3.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov3.rfl — additional targeted coverage for collection.c remaining gaps ;; Focuses on: atom_eq LIST path, propagate_sym_dict, list_to_typed_vec empty SYM/STR, ;; take STR range out-of-bounds, take dict with typed vals, diff --git a/test/rfl/collection/cov4.rfl b/test/rfl/collection/cov4.rfl index 61b0a9bb..07923cbf 100644 --- a/test/rfl/collection/cov4.rfl +++ b/test/rfl/collection/cov4.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov4 — targeted coverage for collection.c remaining gaps ;; Focuses on: atom_eq different-length vecs, range-take type errors, ;; STR typed vec from CSV, STR range-take out-of-bounds, diff --git a/test/rfl/collection/cov5.rfl b/test/rfl/collection/cov5.rfl index 0dd92350..2be5c80b 100644 --- a/test/rfl/collection/cov5.rfl +++ b/test/rfl/collection/cov5.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov5 — targeted coverage: distinct_sort_cmp default branch (lines 282-291) ;; ;; F32 (type=6) is not in hs_hash_row switch → hashes by index (all "distinct"). diff --git a/test/rfl/collection/cov6.rfl b/test/rfl/collection/cov6.rfl index d6517d32..efd9d21b 100644 --- a/test/rfl/collection/cov6.rfl +++ b/test/rfl/collection/cov6.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cov6 — targeted coverage: parted_to_flat_vec STR path (lines 778-790) ;; ;; parted_to_flat_vec has two branches: diff --git a/test/rfl/group/group_key_types.rfl b/test/rfl/group/group_key_types.rfl index 8c868fe2..aca43f3f 100644 --- a/test/rfl/group/group_key_types.rfl +++ b/test/rfl/group/group_key_types.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for group.c — key type diversity paths ;; ;; Targets: diff --git a/test/rfl/io/csv_branch_cov.rfl b/test/rfl/io/csv_branch_cov.rfl index 562e0e21..93eead2d 100644 --- a/test/rfl/io/csv_branch_cov.rfl +++ b/test/rfl/io/csv_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Branch coverage for src/io/csv.c — targets reachable functional branches ;; left uncovered by csv_types.rfl, csv_round2.rfl, csv_splayed.rfl, ;; system/{read,write}_csv.rfl, and test/test_csv.c. diff --git a/test/rfl/io/csv_parallel_scan.rfl b/test/rfl/io/csv_parallel_scan.rfl index d8b0e737..470235d4 100644 --- a/test/rfl/io/csv_parallel_scan.rfl +++ b/test/rfl/io/csv_parallel_scan.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Parallel row-offset scan (csv.c build_row_offsets_par) — exactness. ;; ;; The scan splits the file into chunks and reconciles quote parity across diff --git a/test/rfl/io/csv_rayfall_temporal.rfl b/test/rfl/io/csv_rayfall_temporal.rfl index 5163c192..6f18420c 100644 --- a/test/rfl/io/csv_rayfall_temporal.rfl +++ b/test/rfl/io/csv_rayfall_temporal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; CSV type inference must recognize Rayfall's display temporal forms. ;; These are the forms users see when DATE/TIMESTAMP values are printed. (.sys.exec "printf 'd,ts\n2024.01.02,2024.01.02D01:02:03.004005006\n' > rf_test_csv_rayfall_temporal.csv") -- 0 diff --git a/test/rfl/io/csv_types.rfl b/test/rfl/io/csv_types.rfl index 671f7366..f2a63358 100644 --- a/test/rfl/io/csv_types.rfl +++ b/test/rfl/io/csv_types.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for src/io/csv.c — type inference, edge cases, parted writer. ;; ;; Targets (by approximate line number): diff --git a/test/rfl/io/read_until_eof.rfl b/test/rfl/io/read_until_eof.rfl index a4ea55ab..2fe117c3 100644 --- a/test/rfl/io/read_until_eof.rfl +++ b/test/rfl/io/read_until_eof.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; `read` / `read-bytes` must read until EOF, not to the size the file ;; reports — issue #572. ;; diff --git a/test/rfl/journal/ops_journal.rfl b/test/rfl/journal/ops_journal.rfl index b962325c..0c04bd19 100644 --- a/test/rfl/journal/ops_journal.rfl +++ b/test/rfl/journal/ops_journal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage extension for src/ops/journal.c. ;; ;; The bulk of src/ops/journal.c is exercised by diff --git a/test/rfl/journal/ops_journal_purge.rfl b/test/rfl/journal/ops_journal_purge.rfl index e24369c1..f21da713 100644 --- a/test/rfl/journal/ops_journal_purge.rfl +++ b/test/rfl/journal/ops_journal_purge.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for .log.purge (src/ops/journal.c ray_log_purge_fn -> ;; src/store/journal.c ray_journal_purge) — issue #279. ;; diff --git a/test/rfl/lang/guid_entropy.rfl b/test/rfl/lang/guid_entropy.rfl index b77ceadb..6d4e5511 100644 --- a/test/rfl/lang/guid_entropy.rfl +++ b/test/rfl/lang/guid_entropy.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; `guid` must be unique across processes — issue #571. ;; ;; The generator seeded its per-thread xorshift state from rand(), and diff --git a/test/rfl/lang/parse_branch_cov.rfl b/test/rfl/lang/parse_branch_cov.rfl index 046871d5..44ca9bf0 100644 --- a/test/rfl/lang/parse_branch_cov.rfl +++ b/test/rfl/lang/parse_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Branch-coverage extension for src/lang/parse.c — the Rayfall parser ;; (tokenizer, atom-literal parsing, list/dict/vector syntax, comments, ;; escape sequences, error recovery). diff --git a/test/rfl/linkop/coverage.rfl b/test/rfl/linkop/coverage.rfl index 6aebc2bb..6bba5bea 100644 --- a/test/rfl/linkop/coverage.rfl +++ b/test/rfl/linkop/coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage workout for src/ops/linkop.c ;; Targets the regions NOT exercised by test/test_link.c: ;; - ray_col_link_fn error paths (lines 291, 293) diff --git a/test/rfl/null/slice_has_nulls.rfl b/test/rfl/null/slice_has_nulls.rfl index c30a383e..60e0e980 100644 --- a/test/rfl/null/slice_has_nulls.rfl +++ b/test/rfl/null/slice_has_nulls.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; slice_has_nulls.rfl — a slice sees its parent's nulls, in memory and on ;; disk (#495). ;; diff --git a/test/rfl/null/sort_null_placement.rfl b/test/rfl/null/sort_null_placement.rfl index 7f11b4a4..6a71ba30 100644 --- a/test/rfl/null/sort_null_placement.rfl +++ b/test/rfl/null/sort_null_placement.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Where a null lands in a sort, pinned for every path that can run. ;; ;; A null is the SMALLEST value: ascending puts nulls first, descending last. diff --git a/test/rfl/null/sym_str_null_compare.rfl b/test/rfl/null/sym_str_null_compare.rfl index 40a13b4f..bde1fa4e 100644 --- a/test/rfl/null/sym_str_null_compare.rfl +++ b/test/rfl/null/sym_str_null_compare.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Equality comparison on NULLABLE SYM / STR columns — fused path vs unfused. ;; ;; A SYM null is sym id 0 and a STR null is a zero-length descriptor: both are diff --git a/test/rfl/ops/internal_coverage.rfl b/test/rfl/ops/internal_coverage.rfl index 15633da0..a033b33a 100644 --- a/test/rfl/ops/internal_coverage.rfl +++ b/test/rfl/ops/internal_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage for src/ops/internal.h static-inline helpers that are ;; instantiated in production TUs (exec.c, filter.c, expr.c, etc.) ;; but have never been exercised through the test suite. diff --git a/test/rfl/query/update_by_sym_width.rfl b/test/rfl/query/update_by_sym_width.rfl index dc52d022..c9b3d38a 100644 --- a/test/rfl/query/update_by_sym_width.rfl +++ b/test/rfl/query/update_by_sym_width.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Regression: `update ... by: ` on a narrow-width SYM key column. ;; ;; SYM columns use an adaptive dictionary-index width (W8/W16/W32/W64 in attrs, diff --git a/test/rfl/regress/load_select_isnull_mask.rfl b/test/rfl/regress/load_select_isnull_mask.rfl index d0fffb14..db46bbd4 100644 --- a/test/rfl/regress/load_select_isnull_mask.rfl +++ b/test/rfl/regress/load_select_isnull_mask.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Regression: load → select ISNULL WHERE-mask corruption. ;; ;; exec_elementwise_unary once had dedicated ISNULL kernels for only some diff --git a/test/rfl/sort/sort_coverage2.rfl b/test/rfl/sort/sort_coverage2.rfl index 3d1dec16..e0b34c28 100644 --- a/test/rfl/sort/sort_coverage2.rfl +++ b/test/rfl/sort/sort_coverage2.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Pass-7 additional sort.c coverage. ;; ;; Targets uncovered regions NOT hit by sort_coverage.rfl: diff --git a/test/rfl/storage/shared_sym_domain.rfl b/test/rfl/storage/shared_sym_domain.rfl index 119d4901..780677f7 100644 --- a/test/rfl/storage/shared_sym_domain.rfl +++ b/test/rfl/storage/shared_sym_domain.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; The client layout, end-to-end through the language surface (sym-domain ;; architecture, Task 7b): a parted `hist` plus a splayed `live` sharing ;; ONE symfile (root/.sym) — write, read, query, join (same-domain fast diff --git a/test/rfl/storage/splay_coverage.rfl b/test/rfl/storage/splay_coverage.rfl index ffc09d7f..51b1be34 100644 --- a/test/rfl/storage/splay_coverage.rfl +++ b/test/rfl/storage/splay_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage extension for src/store/splay.c. ;; ;; src/store/splay.c is exercised by: diff --git a/test/rfl/store/col_format_generation.rfl b/test/rfl/store/col_format_generation.rfl index a5d3cccc..d9b2de41 100644 --- a/test/rfl/store/col_format_generation.rfl +++ b/test/rfl/store/col_format_generation.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; On-disk column format generation (the header `order` byte, offset 17). ;; ;; Regression: an engine build briefly stamped the generation to 1 and demanded diff --git a/test/rfl/store/indexed_col_aux_refs.rfl b/test/rfl/store/indexed_col_aux_refs.rfl index 962539ea..0d2731be 100644 --- a/test/rfl/store/indexed_col_aux_refs.rfl +++ b/test/rfl/store/indexed_col_aux_refs.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; An indexed column still owns what sits at aux bytes 8-15. ;; ;; A SYM column keeps its resolution domain there, a STR column its pool. diff --git a/test/rfl/strop/like_seen_proj.rfl b/test/rfl/strop/like_seen_proj.rfl index 84aa9ec2..5e78351f 100644 --- a/test/rfl/strop/like_seen_proj.rfl +++ b/test/rfl/strop/like_seen_proj.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Targeted parallel SYM-LIKE coverage for like_seen_fn / like_proj_fn. ;; ;; Both kernels are the worker bodies dispatched by ray_pool_dispatch diff --git a/test/rfl/strop/string_branch_cov.rfl b/test/rfl/strop/string_branch_cov.rfl index 24838bfb..27b26bb8 100644 --- a/test/rfl/strop/string_branch_cov.rfl +++ b/test/rfl/strop/string_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; string_branch_cov.rfl -- targeted branch coverage for src/ops/string.c ;; ;; Baseline: 61.41% (345 uncovered branches). This file targets the diff --git a/test/rfl/symbol/sym_coverage.rfl b/test/rfl/symbol/sym_coverage.rfl index 1b9e2b4a..01a23e7d 100644 --- a/test/rfl/symbol/sym_coverage.rfl +++ b/test/rfl/symbol/sym_coverage.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Coverage extension for src/table/sym.c. ;; ;; src/table/sym.c is exercised by many existing tests via CSV/splayed I/O. diff --git a/test/rfl/system/cli_flag_values.rfl b/test/rfl/system/cli_flag_values.rfl index c014eada..2eb4e703 100644 --- a/test/rfl/system/cli_flag_values.rfl +++ b/test/rfl/system/cli_flag_values.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; cli_flag_values.rfl — a value-taking flag must not swallow the next flag, ;; and an unknown option must not be mistaken for the script (#600). ;; diff --git a/test/rfl/system/csv_auto_int_width.rfl b/test/rfl/system/csv_auto_int_width.rfl index fd8ff119..91c37ea7 100644 --- a/test/rfl/system/csv_auto_int_width.rfl +++ b/test/rfl/system/csv_auto_int_width.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; `INT` schema token → auto narrowest integer width on .csv.splayed. ;; ;; A column declared `INT` in a .csv.splayed schema is parsed as int64, diff --git a/test/rfl/system/csv_explicit_numeric_types.rfl b/test/rfl/system/csv_explicit_numeric_types.rfl index d053c520..facf8928 100644 --- a/test/rfl/system/csv_explicit_numeric_types.rfl +++ b/test/rfl/system/csv_explicit_numeric_types.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Explicit numeric schemas must survive the streaming .csv.splayed writer ;; and the mmap-backed .db.splayed.get reload without widening. diff --git a/test/rfl/system/db_get.rfl b/test/rfl/system/db_get.rfl index cb03cd4a..66f6cf33 100644 --- a/test/rfl/system/db_get.rfl +++ b/test/rfl/system/db_get.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Multi-table roots through the explicit .db trio — the surface left ;; after the .db.*.mount removal: every table is opened by name with ;; .db.splayed.get / .db.parted.get; nothing is discovered or bound diff --git a/test/rfl/system/db_parted_fill.rfl b/test/rfl/system/db_parted_fill.rfl index 2574b271..b0e3a947 100644 --- a/test/rfl/system/db_parted_fill.rfl +++ b/test/rfl/system/db_parted_fill.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; .db.parted.fill — fill missing tables across a parted db's partitions. ;; ;; For every table present in ANY partition, write an empty copy (schema diff --git a/test/rfl/system/db_sym_resolution.rfl b/test/rfl/system/db_sym_resolution.rfl index daed3ae8..56fce717 100644 --- a/test/rfl/system/db_sym_resolution.rfl +++ b/test/rfl/system/db_sym_resolution.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; SYM-domain symfile resolution precedence, end-to-end (sym-domain ;; architecture spec, "Surface"): ;; explicit argument | dir/.sym | partition-shaped parent -> root/.sym diff --git a/test/rfl/system/ipc_diff.rfl b/test/rfl/system/ipc_diff.rfl index e02f1f08..31f627bd 100644 --- a/test/rfl/system/ipc_diff.rfl +++ b/test/rfl/system/ipc_diff.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Differential IPC oracle. ;; ;; Spawns a fresh `./rayforce -p PORT` process as an IPC server, then diff --git a/test/rfl/system/ipc_first_last.rfl b/test/rfl/system/ipc_first_last.rfl index fcd36642..b468e0bd 100644 --- a/test/rfl/system/ipc_first_last.rfl +++ b/test/rfl/system/ipc_first_last.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Regression: `first`/`last` returned over IPC used to hang the client ;; forever (issue #285). ;; diff --git a/test/rfl/system/ipc_open_errors.rfl b/test/rfl/system/ipc_open_errors.rfl index 525da04f..7760319a 100644 --- a/test/rfl/system/ipc_open_errors.rfl +++ b/test/rfl/system/ipc_open_errors.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; ipc_open_errors.rfl — `.ipc.open` names the failure it actually hit. ;; ;; Regression (#472): the connect and the wire handshake share one budget, diff --git a/test/rfl/system/ipc_open_timeout.rfl b/test/rfl/system/ipc_open_timeout.rfl index 6a525b1b..896254d6 100644 --- a/test/rfl/system/ipc_open_timeout.rfl +++ b/test/rfl/system/ipc_open_timeout.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Connect timeout argument for `.ipc.open` (issue #286). ;; ;; `.ipc.open` is now a variadic builtin accepting an optional second diff --git a/test/rfl/system/listen_fatal.rfl b/test/rfl/system/listen_fatal.rfl index 7417ae32..d4f93aa4 100644 --- a/test/rfl/system/listen_fatal.rfl +++ b/test/rfl/system/listen_fatal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; listen_fatal.rfl — a `-p` that cannot bind is fatal (#473). ;; ;; Regression: when ray_ipc_listen_at failed, main printed diff --git a/test/rfl/system/load_errors.rfl b/test/rfl/system/load_errors.rfl index f2fdcf82..99959bf6 100644 --- a/test/rfl/system/load_errors.rfl +++ b/test/rfl/system/load_errors.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; load_errors.rfl — a failing `load` names the file and the OS cause (#505). ;; ;; Regression: ray_load_file_fn returned a bare `io` for every failure, so diff --git a/test/rfl/system/load_home.rfl b/test/rfl/system/load_home.rfl index 4ce8575e..febcad8e 100644 --- a/test/rfl/system/load_home.rfl +++ b/test/rfl/system/load_home.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; load_home.rfl — relative `load` paths fall back to RAYFORCE_HOME, and ;; (.sys.args) reports the file being evaluated as `source` (#506). ;; diff --git a/test/rfl/system/log_journal.rfl b/test/rfl/system/log_journal.rfl index bca8730f..a6d1e753 100644 --- a/test/rfl/system/log_journal.rfl +++ b/test/rfl/system/log_journal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Journal end-to-end — transaction-log journaling (-l/-L). ;; ;; Two named processes are spawned in sequence under -l ; this diff --git a/test/rfl/system/log_journal_advanced.rfl b/test/rfl/system/log_journal_advanced.rfl index 7d1a7e22..e0916179 100644 --- a/test/rfl/system/log_journal_advanced.rfl +++ b/test/rfl/system/log_journal_advanced.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Journal feature — invariants the basic test (log_journal.rfl) does ;; not cover. Each phase uses a distinct base + port so a phase ;; failing mid-run doesn't pollute the next one's state. diff --git a/test/rfl/system/os_fs.rfl b/test/rfl/system/os_fs.rfl index 9bccd27e..76630f81 100644 --- a/test/rfl/system/os_fs.rfl +++ b/test/rfl/system/os_fs.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; .fs.size and .fs.list — filesystem metadata primitives, issue #36. ;; ;; Two functions on purpose: every other predicate (exists, is-file, diff --git a/test/rfl/system/part.rfl b/test/rfl/system/part.rfl index 9084f24f..0af05319 100644 --- a/test/rfl/system/part.rfl +++ b/test/rfl/system/part.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; src/store/part.c — exercise every reachable branch of ray_read_parted ;; (the function backing .db.parted.get) plus the ;; static helpers infer_mc_type, parse_date_dir, parse_int_dir, diff --git a/test/rfl/system/part_branch_cov.rfl b/test/rfl/system/part_branch_cov.rfl index 9553fc94..5b71277d 100644 --- a/test/rfl/system/part_branch_cov.rfl +++ b/test/rfl/system/part_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; src/store/part.c — branch-coverage top-up for ray_read_parted and its ;; static helpers (is_date_dir, is_integer_str, infer_mc_type, ;; parse_date_dir, parse_int_dir, collect_part_dirs). diff --git a/test/rfl/system/piped_timers.rfl b/test/rfl/system/piped_timers.rfl index 83cb6a64..689987ce 100644 --- a/test/rfl/system/piped_timers.rfl +++ b/test/rfl/system/piped_timers.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; piped_timers.rfl — timers keep firing while a piped (non-TTY) stdin is ;; held open, and a process stays for its pending timers after input ends ;; (#493, the narrowed report). diff --git a/test/rfl/system/process_identity.rfl b/test/rfl/system/process_identity.rfl index d0baa387..af3e5183 100644 --- a/test/rfl/system/process_identity.rfl +++ b/test/rfl/system/process_identity.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Process and host identity, plus .sys.exec output capture — issue #573. ;; ;; An embedded process could not learn anything about itself: no PID, no diff --git a/test/rfl/system/querylog_ipc.rfl b/test/rfl/system/querylog_ipc.rfl index f65b18c2..08dd04fb 100644 --- a/test/rfl/system/querylog_ipc.rfl +++ b/test/rfl/system/querylog_ipc.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Query-statistics ring end-to-end over IPC (the server / cloud path). ;; ;; The capture hook lives in eval_payload_core (src/core/ipc.c), so it only diff --git a/test/rfl/system/read_csv.rfl b/test/rfl/system/read_csv.rfl index 5ecd1080..fcbe301f 100644 --- a/test/rfl/system/read_csv.rfl +++ b/test/rfl/system/read_csv.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; Ported from test_lang_rf.inc::test_rf_read_csv. ;; Rayfall's str-pool hits "error: limit" when raze/fold accumulates ;; ~1000 strings, so we shell out via .sys.exec to write the 20k-row diff --git a/test/rfl/system/startup_script_fatal.rfl b/test/rfl/system/startup_script_fatal.rfl index 34373856..d935cc27 100644 --- a/test/rfl/system/startup_script_fatal.rfl +++ b/test/rfl/system/startup_script_fatal.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; startup_script_fatal.rfl — a failing startup script under -p is fatal ;; non-interactively (#507). ;; diff --git a/test/rfl/system/system_branch_cov.rfl b/test/rfl/system/system_branch_cov.rfl index 32adf1dd..bb67d9ed 100644 --- a/test/rfl/system/system_branch_cov.rfl +++ b/test/rfl/system/system_branch_cov.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; system_branch_cov.rfl — branch coverage for src/ops/system.c (part 1). ;; ;; Covers: ser/de, splayed set/get, parted get, os.size, os.list, diff --git a/test/rfl/system/timer_overrun.rfl b/test/rfl/system/timer_overrun.rfl index 9bd67729..125b2254 100644 --- a/test/rfl/system/timer_overrun.rfl +++ b/test/rfl/system/timer_overrun.rfl @@ -1,3 +1,4 @@ +;; @requires: posix (fixtures/checks run through a POSIX shell via .sys.exec) ;; timer_overrun.rfl — a periodic timer does not replay missed intervals, ;; and a failing callback's line carries the message (#474). ;; diff --git a/test/stress_store.c b/test/stress_store.c index 71ac326d..c92453e4 100644 --- a/test/stress_store.c +++ b/test/stress_store.c @@ -4,6 +4,11 @@ * Rayforce heap under test. */ +#if !defined(_WIN32) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE /* lstat under strict -std=c17 */ +#endif + +#include "test.h" /* POSIX shims on Windows (lstat) */ #include "stress_store.h" #include "store/splay.h" #include "store/part.h" @@ -14,6 +19,9 @@ #include #include #include /* getpid — per-process scratch paths */ +#include +#include +#include "store/fileio.h" /* ray_mkdir_p */ const char* stress_db_path(const char* name) { static char buf[256]; @@ -148,10 +156,28 @@ void stress_part_dir(const stress_ctx_t* c, int i, char* buf, size_t n) { snprintf(buf, n, "%s/%s/hist", c->db_root, c->part_dates[i]); } +/* Recursive delete without a shell (portable to Windows, where system() + * runs cmd.exe and has no `rm -rf`). */ static void rm_rf(const char* path) { - char cmd[600]; - snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path); - (void)!system(cmd); + struct stat st; + if (lstat(path, &st) != 0) return; /* never follow a symlink out */ + if (S_ISDIR(st.st_mode)) { + DIR* d = opendir(path); + if (d) { + struct dirent* ent; + char child[1024]; + while ((ent = readdir(d)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) + continue; + snprintf(child, sizeof(child), "%s/%s", path, ent->d_name); + rm_rf(child); + } + closedir(d); + } + (void)rmdir(path); + } else { + (void)unlink(path); + } } /* ---- ray table <-> shadow rows ------------------------------------------ */ @@ -276,9 +302,7 @@ bool stress_init(stress_ctx_t* c, const char* db_root, uint64_t seed) { c->oplog = (char(*)[128])malloc((size_t)STRESS_OPLOG_CAP * 128); if (!c->oplog) return false; rm_rf(c->db_root); - char cmd[600]; - snprintf(cmd, sizeof(cmd), "mkdir -p '%s'", c->db_root); - if (system(cmd) != 0) { + if (ray_mkdir_p(c->db_root) != RAY_OK) { free(c->oplog); c->oplog = NULL; return false; diff --git a/test/test.h b/test/test.h index 8a54c42e..8f67db58 100644 --- a/test/test.h +++ b/test/test.h @@ -41,6 +41,60 @@ #include #include +/* Shell-free filesystem helpers (test/main.c). Tests must not depend on + * /bin/sh: on Windows system() runs cmd.exe. Both return 0 on success. */ +int ray_test_rm_rf(const char* path); /* rm -rf path */ +int ray_test_mkdir_p(const char* path); /* mkdir -p path */ + +#if defined(_WIN32) +/* POSIX helpers the tests rely on, mapped onto their MSVCRT equivalents. + * Test paths use "/tmp/...", which Windows resolves to :\tmp; the + * runner creates that directory at startup (see test/main.c). */ +#include +#include +#include +#include +static inline int setenv(const char* k, const char* v, int overwrite) { + if (!overwrite && getenv(k)) return 0; + return _putenv_s(k, v) == 0 ? 0 : -1; +} +static inline int unsetenv(const char* k) { + return _putenv_s(k, "") == 0 ? 0 : -1; /* "" removes the variable */ +} +static inline char* mkdtemp(char* tmpl) { + if (!_mktemp(tmpl)) return NULL; + return _mkdir(tmpl) == 0 ? tmpl : NULL; +} +static inline unsigned geteuid(void) { return 1; } /* never "root" */ +#define lstat stat /* no symlinks to skip */ +#include +#include +static inline int symlink(const char* target, const char* path) { + (void)target; (void)path; + errno = ENOSYS; /* callers skip when symlink fails */ + return -1; +} +#define mkdir(p, mode) _mkdir(p) +#define pipe(fds) _pipe((fds), 65536, _O_BINARY) +/* sysconf subset (page size, physical pages, CPUs); see test/main.c. */ +#define _SC_PAGESIZE 1 +#define _SC_PAGE_SIZE _SC_PAGESIZE +#define _SC_PHYS_PAGES 2 +#define _SC_NPROCESSORS_ONLN 3 +long ray_test_sysconf(int name); +#define sysconf ray_test_sysconf +/* MSVCRT's tmpfile() creates its file in the drive root, which needs admin + * rights. Use the temp directory instead; "D" deletes the file on close. */ +static inline FILE* ray_test_tmpfile(void) { + char* name = _tempnam("/tmp", "rayt"); + if (!name) return NULL; + FILE* f = fopen(name, "w+bD"); + free(name); + return f; +} +#define tmpfile ray_test_tmpfile +#endif + typedef enum { TEST_PASS = 0, TEST_FAIL, TEST_SKIP } test_status_t; typedef struct { diff --git a/test/test_aof.c b/test/test_aof.c index 4705436f..469f5491 100644 --- a/test/test_aof.c +++ b/test/test_aof.c @@ -58,7 +58,12 @@ static void aof_rm_rf(const char* dir) { } static void aof_setup(void) { - snprintf(g_aof_dir, sizeof g_aof_dir, "/tmp/ray_test_aof_%d", (int)getpid()); + /* A fresh dir per test: the crash test deliberately leaks an open + * writer, and on Windows an open file cannot be deleted, so a shared + * dir would hand its stale segment to every later test. */ + static int seq = 0; + snprintf(g_aof_dir, sizeof g_aof_dir, "/tmp/ray_test_aof_%d_%d", + (int)getpid(), seq++); aof_rm_rf(g_aof_dir); } diff --git a/test/test_csv.c b/test/test_csv.c index 9a964fc2..3f015624 100644 --- a/test/test_csv.c +++ b/test/test_csv.c @@ -213,7 +213,7 @@ static test_result_t test_csv_null_i64(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n\n30\n"); fclose(f); @@ -245,7 +245,7 @@ static test_result_t test_csv_null_i64_unparseable(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\nN/A\n30\n"); fclose(f); @@ -274,7 +274,7 @@ static test_result_t test_csv_null_f64(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n1.5\n\n3.5\n"); fclose(f); @@ -305,7 +305,7 @@ static test_result_t test_csv_null_i16(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n\n30\n"); fclose(f); @@ -336,7 +336,7 @@ static test_result_t test_csv_null_i32(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n\n30\n"); fclose(f); @@ -367,7 +367,7 @@ static test_result_t test_csv_null_date(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "d\n2025-01-02\n\n2026-12-31\n"); fclose(f); @@ -396,7 +396,7 @@ static test_result_t test_csv_null_time(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "t\n12:34:56\n\n23:59:59\n"); fclose(f); @@ -425,7 +425,7 @@ static test_result_t test_csv_null_timestamp(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "ts\n2025-01-02T03:04:05\n\n2026-12-31T23:59:59\n"); fclose(f); @@ -455,7 +455,7 @@ static test_result_t test_csv_null_bool(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "flag\ntrue\n\nfalse\n"); fclose(f); @@ -484,7 +484,7 @@ static test_result_t test_csv_null_sym(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "name\nalice\n\nbob\n"); fclose(f); @@ -515,7 +515,7 @@ static test_result_t test_csv_no_nulls_no_null_bitmap(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\n20\n30\n"); fclose(f); @@ -537,7 +537,7 @@ static test_result_t test_csv_null_mixed_columns(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "id,val,name\n1,1.5,alice\n,2.5,\n3,,bob\n"); fclose(f); @@ -579,7 +579,7 @@ static test_result_t test_csv_explicit_str_schema(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); /* Mix inline (<=12B), pooled (>12B), empty/null, and a short */ fprintf(f, "id,note\n" "1,hi\n" @@ -628,7 +628,7 @@ static test_result_t test_csv_escaped_str_roundtrip(void) { (void)ray_sym_init(); /* Write a CSV with fields that require quoting/escaping */ - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "s\n" "\"he,llo\"\n" "\"wo\"\"rld\"\n" @@ -748,7 +748,7 @@ static test_result_t test_csv_infer_date(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "d\n2025-01-02\n2026-12-31\n2000-03-15\n"); fclose(f); @@ -769,7 +769,7 @@ static test_result_t test_csv_infer_time(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "t\n12:34:56\n00:00:00\n23:59:59.123\n"); fclose(f); @@ -792,7 +792,7 @@ static test_result_t test_csv_infer_timestamp_promotion(void) { /* Mix of full timestamps with both 'T' and ' ' separators, plus a * date-only sentinel that should be promoted to TIMESTAMP. */ - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "ts\n2025-01-02T03:04:05\n2025-06-07 08:09:10.123456\n2024-12-31\n"); fclose(f); @@ -813,7 +813,7 @@ static test_result_t test_csv_infer_bool(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "b\ntrue\nfalse\nTRUE\nFALSE\n"); fclose(f); @@ -834,7 +834,7 @@ static test_result_t test_csv_infer_f64_specials(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n1.0\n2e10\n-3.5E-2\nnan\nInf\n+inf\n-INF\n"); fclose(f); @@ -856,7 +856,7 @@ static test_result_t test_csv_infer_null_sentinels(void) { (void)ray_sym_init(); /* Sentinel rows alternating with i64 values; column should infer I64. */ - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n10\nN/A\nNA\nnull\nNULL\nNone\nnone\nn/a\nna\n.\n42\n"); fclose(f); @@ -882,7 +882,7 @@ static test_result_t test_csv_infer_promotions(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "n,b\n1,true\n2,0\n3.5,1\n"); fclose(f); @@ -905,7 +905,7 @@ static test_result_t test_csv_tab_delimiter(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "a\tb\tc\n1\t2\t3\n4\t5\t6\n"); fclose(f); @@ -926,7 +926,7 @@ static test_result_t test_csv_no_header(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "10,20\n30,40\n50,60\n"); fclose(f); @@ -988,7 +988,7 @@ static test_result_t test_csv_invalid_schema_type(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "x\n1\n2\n"); fclose(f); @@ -1002,7 +1002,7 @@ static test_result_t test_csv_invalid_schema_type(void) { /* Schema too short for ncols also errors out. */ int8_t one_only[1] = { RAY_I64 }; - FILE* g = fopen(TMP_CSV, "w"); + FILE* g = fopen(TMP_CSV, "wb"); fprintf(g, "a,b\n1,2\n"); fclose(g); ray_t* loaded3 = ray_read_csv_opts(TMP_CSV, ',', true, one_only, 1); @@ -1044,7 +1044,7 @@ static test_result_t test_csv_truncated_row(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "a,b,c\n1,2,3\n4\n7,8,9\n"); fclose(f); @@ -1277,7 +1277,7 @@ static test_result_t test_csv_parallel_parse(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "i,s\n"); /* 9000 rows so n_rows > 8192. */ for (int i = 0; i < 9000; i++) @@ -1306,7 +1306,7 @@ static test_result_t test_csv_sym_narrowing(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "k\n"); /* Only three distinct values across many rows. */ for (int i = 0; i < 200; i++) @@ -1342,7 +1342,7 @@ static test_result_t test_csv_explicit_u8_schema(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n"); /* 10 000 rows ⇒ parallel parse path; values 0..255 cycling so the * truncated bytes fully exercise the U8 range. */ @@ -1384,7 +1384,7 @@ static test_result_t test_csv_explicit_i16_schema_with_nulls(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n"); const int N = 1500; for (int i = 0; i < N; i++) { @@ -1427,7 +1427,7 @@ static test_result_t test_csv_explicit_i32_schema(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "v\n"); const int N = 500; for (int i = 0; i < N; i++) fprintf(f, "%d\n", -100000 + i * 137); @@ -1460,7 +1460,7 @@ static test_result_t test_csv_explicit_u8_schema_serial(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "a,b\n"); /* 200 rows; second column missing on every 50th row → triggers * past-row-boundary fill in the parser. */ @@ -1501,7 +1501,7 @@ static test_result_t test_csv_infer_high_cardinality_str(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); fprintf(f, "payload\n"); for (int i = 0; i < 100; i++) fprintf(f, "unique_payload_%03d\n", i); @@ -1570,7 +1570,7 @@ static test_result_t test_csv_interrupt_mid_parse(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); TEST_ASSERT_NOT_NULL(f); fputs("id,payload\n", f); for (int i = 0; i < 200000; i++) { @@ -1610,7 +1610,7 @@ static test_result_t test_csv_progress_never_goes_backwards(void) { ray_heap_init(); (void)ray_sym_init(); - FILE* f = fopen(TMP_CSV, "w"); + FILE* f = fopen(TMP_CSV, "wb"); TEST_ASSERT_NOT_NULL(f); fputs("payload,symbol\n", f); for (int i = 0; i < 100000; i++) diff --git a/test/test_heap.c b/test/test_heap.c index 2054f7b1..0e14473f 100644 --- a/test/test_heap.c +++ b/test/test_heap.c @@ -50,7 +50,33 @@ #include #include #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +/* Anonymous mmap for the fake file-mapped (mmod==1) blocks below: a + * pagefile-backed view, so the library's ray_vm_unmap_file + * (UnmapViewOfFile) releases it just as it would a real file mapping. */ +#define PROT_READ 1 +#define PROT_WRITE 2 +#define MAP_PRIVATE 2 +#define MAP_ANONYMOUS 0x20 +#define MAP_FAILED ((void*)-1) +static void* mmap(void* addr, size_t len, int prot, int flags, int fd, long off) { + (void)addr; (void)prot; (void)flags; (void)fd; (void)off; + HANDLE m = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, + 0, (DWORD)len, NULL); + if (!m) return MAP_FAILED; + void* p = MapViewOfFile(m, FILE_MAP_WRITE, 0, 0, len); + CloseHandle(m); + return p ? p : MAP_FAILED; +} +static int munmap(void* p, size_t len) { + (void)len; + return UnmapViewOfFile(p) ? 0 : -1; +} +#else #include +#endif #include #include @@ -2565,6 +2591,11 @@ static test_result_t test_direct_cache_concurrent_replacement(void) { /* Drive the anon-to-file crossing with a low watermark, independent of RAM. */ static test_result_t test_anon_watermark_spill(void) { +#if defined(_WIN32) + /* Windows takes only the anonymous path; the file-backed spill is + * POSIX-only (docs/architecture/memory.md, RAY_HEAP_FILE_SPILL). */ + SKIP("file-backed spill is POSIX-only"); +#endif size_t sz = 40 * 1024 * 1024 - 128; /* order 26 → direct path */ /* Start from an empty reuse cache: leftover cached blocks from earlier * tests would (a) inflate the baseline and (b) be drained by the diff --git a/test/test_ipc.c b/test/test_ipc.c index 21b3dfa1..aa1752f0 100644 --- a/test/test_ipc.c +++ b/test/test_ipc.c @@ -54,11 +54,16 @@ #include "test.h" #include "ipc_harness.h" +#ifdef RAY_OS_WINDOWS +#include +#include +#else #include #include #include #include #include +#endif #include #include "core/ipc.h" #include "core/sock.h" @@ -1047,12 +1052,17 @@ static test_result_t test_ipc_send_large_compressible(void) { * Open a journal, then connect an IPC server on top; each SYNC message * should flow through ray_journal_write_bytes. */ +/* Drop a journal's .log / .qdb pair (no shell). */ +static void ipc_rm_journal(const char* jbase) { + char path[256]; + snprintf(path, sizeof(path), "%s.log", jbase); (void)remove(path); + snprintf(path, sizeof(path), "%s.qdb", jbase); (void)remove(path); +} + static test_result_t test_ipc_journal_path(void) { const char* jbase = "/tmp/rayforce_test_ipc_journal"; /* Remove stale files */ - char cmd[256]; - snprintf(cmd, sizeof(cmd), "rm -f %s.log %s.qdb", jbase, jbase); - system(cmd); + ipc_rm_journal(jbase); /* Open journal */ ray_err_t jerr = ray_journal_open(jbase, RAY_JOURNAL_ASYNC); @@ -1081,7 +1091,7 @@ static test_result_t test_ipc_journal_path(void) { ray_test_server_stop(&srv); ray_journal_close(); - system(cmd); /* cleanup */ + ipc_rm_journal(jbase); /* cleanup */ PASS(); } @@ -1453,9 +1463,7 @@ static test_result_t test_ipc_send_verbose_large_result(void) { */ static test_result_t test_ipc_journal_restricted(void) { const char* jbase = "/tmp/rayforce_test_ipc_jrestr"; - char cmd[256]; - snprintf(cmd, sizeof(cmd), "rm -f %s.log %s.qdb", jbase, jbase); - system(cmd); + ipc_rm_journal(jbase); ray_err_t jerr = ray_journal_open(jbase, RAY_JOURNAL_ASYNC); if (jerr != RAY_OK) { @@ -1486,7 +1494,7 @@ static test_result_t test_ipc_journal_restricted(void) { ray_test_server_stop(&srv); ray_journal_close(); - system(cmd); + ipc_rm_journal(jbase); PASS(); } @@ -1979,6 +1987,7 @@ static test_result_t test_ipc_addr_local_ipv6_remote(void) { PASS(); } +#ifndef RAY_OS_WINDOWS /* AF_UNIX locality is POSIX-only (see sock.c) */ static test_result_t test_ipc_addr_local_af_unix(void) { struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); @@ -1986,6 +1995,7 @@ static test_result_t test_ipc_addr_local_af_unix(void) { TEST_ASSERT_TRUE(ray_sock_addr_is_local(&sa, sizeof(sa))); PASS(); } +#endif static test_result_t test_ipc_addr_local_rejects_garbage(void) { struct sockaddr_in sa; @@ -1998,24 +2008,53 @@ static test_result_t test_ipc_addr_local_rejects_garbage(void) { PASS(); } -/* A real AF_UNIX pair resolves as local through getpeername. */ +/* A connected pair of sockets on this machine: an AF_UNIX socketpair on + * POSIX; Windows has no socketpair(2), so a loopback TCP pair there. */ +static int test_local_sock_pair(ray_sock_t sv[2]) { +#ifdef RAY_OS_WINDOWS + ray_sock_t srv = ray_sock_listen_at("127.0.0.1", 0); + if (srv == RAY_INVALID_SOCK) return -1; + struct sockaddr_in addr; + int len = sizeof(addr); + if (getsockname((SOCKET)srv, (struct sockaddr*)&addr, &len) != 0) { + ray_sock_close(srv); + return -1; + } + sv[0] = ray_sock_connect("127.0.0.1", ntohs(addr.sin_port), 0); + sv[1] = sv[0] == RAY_INVALID_SOCK ? RAY_INVALID_SOCK : ray_sock_accept(srv); + ray_sock_close(srv); + if (sv[1] == RAY_INVALID_SOCK) { + if (sv[0] != RAY_INVALID_SOCK) ray_sock_close(sv[0]); + return -1; + } + return 0; +#else + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) return -1; + sv[0] = fds[0]; + sv[1] = fds[1]; + return 0; +#endif +} + +/* A real local pair resolves as local through getpeername. */ static test_result_t test_ipc_peer_is_local_socketpair(void) { - int sv[2]; - TEST_ASSERT_EQ_I(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); - TEST_ASSERT_TRUE(ray_sock_peer_is_local((ray_sock_t)sv[0])); - TEST_ASSERT_TRUE(ray_sock_peer_is_local((ray_sock_t)sv[1])); - close(sv[0]); - close(sv[1]); + ray_sock_t sv[2]; + TEST_ASSERT_EQ_I(test_local_sock_pair(sv), 0); + TEST_ASSERT_TRUE(ray_sock_peer_is_local(sv[0])); + TEST_ASSERT_TRUE(ray_sock_peer_is_local(sv[1])); + ray_sock_close(sv[0]); + ray_sock_close(sv[1]); PASS(); } /* An unconnected socket has no peer: getpeername fails, and an unknown * peer must fall back to the compressing default, not to "local". */ static test_result_t test_ipc_peer_is_local_unconnected(void) { - int fd = socket(AF_INET, SOCK_STREAM, 0); - TEST_ASSERT_TRUE(fd >= 0); - TEST_ASSERT_FALSE(ray_sock_peer_is_local((ray_sock_t)fd)); - close(fd); + ray_sock_t fd = (ray_sock_t)socket(AF_INET, SOCK_STREAM, 0); + TEST_ASSERT_TRUE(fd != RAY_INVALID_SOCK); + TEST_ASSERT_FALSE(ray_sock_peer_is_local(fd)); + ray_sock_close(fd); PASS(); } @@ -2027,18 +2066,18 @@ static test_result_t test_ipc_peer_is_local_invalid_fd(void) { /* The policy the send paths consult: local links never compress, others * keep the compiled-in default. */ static test_result_t test_ipc_link_threshold_local_vs_remote(void) { - int sv[2]; - TEST_ASSERT_EQ_I(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0); - TEST_ASSERT_EQ_U(ray_ipc_link_threshold((ray_sock_t)sv[0]), + ray_sock_t sv[2]; + TEST_ASSERT_EQ_I(test_local_sock_pair(sv), 0); + TEST_ASSERT_EQ_U(ray_ipc_link_threshold(sv[0]), RAY_IPC_COMPRESS_NEVER); - close(sv[0]); - close(sv[1]); + ray_sock_close(sv[0]); + ray_sock_close(sv[1]); - int fd = socket(AF_INET, SOCK_STREAM, 0); - TEST_ASSERT_TRUE(fd >= 0); - TEST_ASSERT_EQ_U(ray_ipc_link_threshold((ray_sock_t)fd), + ray_sock_t fd = (ray_sock_t)socket(AF_INET, SOCK_STREAM, 0); + TEST_ASSERT_TRUE(fd != RAY_INVALID_SOCK); + TEST_ASSERT_EQ_U(ray_ipc_link_threshold(fd), (size_t)RAY_IPC_COMPRESS_THRESHOLD); - close(fd); + ray_sock_close(fd); PASS(); } @@ -2747,7 +2786,9 @@ const test_entry_t ipc_entries[] = { { "ipc/addr_local/ipv6_loopback", test_ipc_addr_local_ipv6_loopback, ipc_setup, ipc_teardown }, { "ipc/addr_local/ipv6_mapped_loopback",test_ipc_addr_local_ipv6_mapped_loopback, ipc_setup, ipc_teardown }, { "ipc/addr_local/ipv6_remote", test_ipc_addr_local_ipv6_remote, ipc_setup, ipc_teardown }, +#ifndef RAY_OS_WINDOWS { "ipc/addr_local/af_unix", test_ipc_addr_local_af_unix, ipc_setup, ipc_teardown }, +#endif { "ipc/addr_local/rejects_garbage", test_ipc_addr_local_rejects_garbage, ipc_setup, ipc_teardown }, { "ipc/peer_is_local/socketpair", test_ipc_peer_is_local_socketpair, ipc_setup, ipc_teardown }, { "ipc/peer_is_local/unconnected", test_ipc_peer_is_local_unconnected, ipc_setup, ipc_teardown }, diff --git a/test/test_journal.c b/test/test_journal.c index 8277a729..9904893f 100644 --- a/test/test_journal.c +++ b/test/test_journal.c @@ -40,7 +40,44 @@ #include #include #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +/* Minimal glob(3) for the archive checks below: wildcards only in the + * last path component, which is all ".*.log" needs. */ +typedef struct { size_t gl_pathc; char** gl_pathv; } glob_t; +static void globfree(glob_t* g) { + for (size_t i = 0; i < g->gl_pathc; i++) free(g->gl_pathv[i]); + free(g->gl_pathv); + g->gl_pathc = 0; g->gl_pathv = NULL; +} +static int glob(const char* pat, int flags, void* errfunc, glob_t* g) { + (void)flags; (void)errfunc; + g->gl_pathc = 0; g->gl_pathv = NULL; + const char* slash = strrchr(pat, '/'); + const char* bs = strrchr(pat, '\\'); + if (bs && (!slash || bs > slash)) slash = bs; + size_t dlen = slash ? (size_t)(slash - pat) + 1 : 0; + WIN32_FIND_DATAA fd; + HANDLE h = FindFirstFileA(pat, &fd); + if (h == INVALID_HANDLE_VALUE) return 3; /* GLOB_NOMATCH */ + do { + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; + size_t nlen = strlen(fd.cFileName); + char* path = (char*)malloc(dlen + nlen + 1); + char** v = (char**)realloc(g->gl_pathv, (g->gl_pathc + 1) * sizeof(char*)); + if (!path || !v) { free(path); if (v) g->gl_pathv = v; FindClose(h); globfree(g); return 1; } + memcpy(path, pat, dlen); + memcpy(path + dlen, fd.cFileName, nlen + 1); + g->gl_pathv = v; + g->gl_pathv[g->gl_pathc++] = path; + } while (FindNextFileA(h, &fd)); + FindClose(h); + return g->gl_pathc ? 0 : 3; +} +#else #include +#endif /* ── Runtime fixture (same pattern as test_link.c) ─────────────────── */ @@ -98,8 +135,17 @@ static void cleanup_base(const char* base) { snprintf(path, sizeof(path), "%s.qdb", base); unlink(path); snprintf(path, sizeof(path), "%s.qdb.tmp", base); unlink(path); /* Archived rolls have the form base..log — remove with glob via shell. */ +#if defined(_WIN32) + snprintf(path, sizeof(path), "%s.*.log", base); /* no POSIX shell here */ + glob_t g; + if (glob(path, 0, NULL, &g) == 0) { + for (size_t i = 0; i < g.gl_pathc; i++) unlink(g.gl_pathv[i]); + globfree(&g); + } +#else snprintf(path, sizeof(path), "rm -f '%s'.*.log 2>/dev/null", base); (void)system(path); +#endif } /* ═══════════════════════════════════════════════════════════════════════ @@ -1041,15 +1087,19 @@ static test_result_t test_journal_crash_window_no_double_apply(void) { /* Simulate the crash IN the window: put the just-archived log back * under its live name, exactly the on-disk state a crash between - * the two renames leaves behind. */ + * the two renames leaves behind. The journal is closed first (the + * "process" is gone), and the live file removed before the rename: + * Windows can neither replace a file that is still open nor rename + * onto an existing one. */ + TEST_ASSERT_EQ_I(ray_journal_close(), RAY_OK); char pattern[300]; snprintf(pattern, sizeof(pattern), "%s.*.log", base); glob_t g; TEST_ASSERT_EQ_I(glob(pattern, 0, NULL, &g), 0); TEST_ASSERT_EQ_I((int64_t)g.gl_pathc, 1); + (void)remove(lpath); TEST_ASSERT_EQ_I(rename(g.gl_pathv[0], lpath), 0); globfree(&g); - TEST_ASSERT_EQ_I(ray_journal_close(), RAY_OK); /* Restart: clobber the binding, recover. The covered log must be * skipped — jw_x comes back as the snapshot value, not value+1. */ @@ -2294,9 +2344,13 @@ static bool purge_write_one(int64_t x) { /* True iff at least one rolled archive (base..log) exists. */ static bool archive_exists(const char* base) { - char cmd[1200]; - snprintf(cmd, sizeof(cmd), "test -n \"$(ls '%s'.*.log 2>/dev/null)\"", base); - return system(cmd) == 0; + char pattern[1200]; + snprintf(pattern, sizeof(pattern), "%s.*.log", base); + glob_t g; + if (glob(pattern, 0, NULL, &g) != 0) return false; /* no match */ + bool found = g.gl_pathc > 0; + globfree(&g); + return found; } /* P1. Full purge while the journal is OPEN: closes it, unlinks the active diff --git a/test/test_link.c b/test/test_link.c index cd430538..7f1c8d56 100644 --- a/test/test_link.c +++ b/test/test_link.c @@ -918,9 +918,7 @@ static ray_err_t write_link_partition(const char* part_dir, int64_t custs_sym) { char dir[1024]; snprintf(dir, sizeof(dir), TMP_LINK_PART_DB "/%s/" TMP_LINK_PART_TBL, part_dir); - char cmd[1100]; - snprintf(cmd, sizeof(cmd), "mkdir -p %s", dir); - if (system(cmd) != 0) return RAY_ERR_IO; + if (ray_test_mkdir_p(dir) != 0) return RAY_ERR_IO; ray_t* ridcol = ray_vec_from_raw(RAY_I64, (void*)rids, n_rid); if (!ridcol || RAY_IS_ERR(ridcol)) return RAY_ERR_OOM; @@ -954,7 +952,7 @@ static ray_err_t write_link_partition(const char* part_dir, static test_result_t test_link_parted_load_propagates(void) { int64_t custs_sym = setup_custs_dim(); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1, 2 }; int64_t q1[] = { 10, 20, 30 }; @@ -1020,7 +1018,7 @@ static test_result_t test_link_parted_load_propagates(void) { ray_release(ages1); ray_release(parted); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1032,7 +1030,7 @@ static test_result_t test_link_attach_rejects_parted_target(void) { /* Build a parted table on disk and load via ray_read_parted so we have a * real RAY_TABLE-with-RAY_PARTED-cols handle to point at. */ int64_t custs_sym = setup_custs_dim(); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1, 2 }; int64_t q1[] = { 10, 20, 30 }; @@ -1065,7 +1063,7 @@ static test_result_t test_link_attach_rejects_parted_target(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1095,7 +1093,7 @@ static test_result_t test_link_deref_rejects_parted_after_rebind(void) { ray_release(good); /* Build a parted table on disk and rebind `custs` to it. */ - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1 }; int64_t q1[] = { 10, 20 }; TEST_ASSERT_EQ_I(write_link_partition("2024.01.01", r1, 2, q1, 2, custs_sym), RAY_OK); @@ -1118,7 +1116,7 @@ static test_result_t test_link_deref_rejects_parted_after_rebind(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1155,7 +1153,7 @@ static test_result_t test_link_dotted_resolve_propagates_parted_error(void) { ray_release(good); /* Rebind custs to a parted table on disk. */ - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1 }; int64_t q1[] = { 10, 20 }; TEST_ASSERT_EQ_I(write_link_partition("2024.01.01", r1, 2, q1, 2, custs_sym), RAY_OK); @@ -1180,7 +1178,7 @@ static test_result_t test_link_dotted_resolve_propagates_parted_error(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } @@ -1212,7 +1210,7 @@ static test_result_t test_link_vm_eval_propagates_parted_error(void) { ray_release(good); /* Rebind custs to a parted table. */ - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); int64_t r1[] = { 0, 1 }; int64_t q1[] = { 10, 20 }; TEST_ASSERT_EQ_I(write_link_partition("2024.01.01", r1, 2, q1, 2, custs_sym), RAY_OK); @@ -1233,7 +1231,7 @@ static test_result_t test_link_vm_eval_propagates_parted_error(void) { ray_release(w); ray_release(v); - (void)!system("rm -rf " TMP_LINK_PART_DB); + (void)ray_test_rm_rf(TMP_LINK_PART_DB); PASS(); } diff --git a/test/test_mcast.c b/test/test_mcast.c index 8fa81053..e71e174b 100644 --- a/test/test_mcast.c +++ b/test/test_mcast.c @@ -26,6 +26,9 @@ #include #include #include +#else + #include + #include #endif extern ray_runtime_t* __RUNTIME; @@ -631,7 +634,6 @@ static test_result_t test_mcast_large_payload_queues_until_writable(void) { TEST_ASSERT_FALSE(RAY_IS_ERR(sub)); ray_release(sub); -#ifndef RAY_OS_WINDOWS ray_t* server_h = ray_env_get(ray_sym_intern("_mc_sub_handle", 14)); TEST_ASSERT_NOT_NULL(server_h); TEST_ASSERT_EQ_I(server_h->type, -RAY_I64); @@ -639,8 +641,7 @@ static test_result_t test_mcast_large_payload_queues_until_writable(void) { TEST_ASSERT_NOT_NULL(server_sel); int sndbuf = 4096; setsockopt((ray_sock_t)server_sel->fd, SOL_SOCKET, SO_SNDBUF, - &sndbuf, sizeof(sndbuf)); -#endif + (const char*)&sndbuf, sizeof(sndbuf)); const char* pub_src = "(.mc.pub \"big\" (+ (* (til 100000) 1103515245) 12345))"; @@ -728,7 +729,6 @@ static test_result_t test_mcast_shared_frame_across_subscribers(void) { int64_t hp = ray_ipc_connect("127.0.0.1", port, NULL, NULL, 0); TEST_ASSERT((hp) >= (0), "publisher connected"); -#ifndef RAY_OS_WINDOWS ray_t* server_hs = ray_env_get(ray_sym_intern("_mc_handles", 11)); TEST_ASSERT_NOT_NULL(server_hs); TEST_ASSERT((ray_len(server_hs)) >= (3), "three server-side subscriber handles"); @@ -737,9 +737,8 @@ static test_result_t test_mcast_shared_frame_across_subscribers(void) { ray_selector_t* ssel = ray_poll_get(poll, sh); TEST_ASSERT_NOT_NULL(ssel); int sndbuf = 4096; - setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, (const char*)&sndbuf, sizeof(sndbuf)); } -#endif const char* pub_src = "(.mc.pub \"big\" (+ (* (til 100000) 1103515245) 12345))"; ray_t* msg = ray_str(pub_src, strlen(pub_src)); @@ -802,6 +801,13 @@ static test_result_t test_mcast_shared_frame_across_subscribers(void) { * frame keeps receiving. The active limits and the high-water mark are * readable from .mc.stats and per handle from (.ipc.handle h). */ static test_result_t test_mcast_txlimit_overflow_disconnects(void) { +#if defined(_WIN32) + /* The backlog this test needs never forms on Windows: Winsock accepts a + * single non-blocking send() larger than SO_SNDBUF whole (it pins the + * caller's buffer), so the 800 KiB frame leaves at once and no + * subscriber crosses its tx limit. */ + SKIP("Winsock accepts oversized sends whole; no tx backlog forms"); +#endif ray_t* r = ray_eval_str( "(set _mc_count 0)" "(set _mc_close_count 0)" @@ -840,14 +846,12 @@ static test_result_t test_mcast_txlimit_overflow_disconnects(void) { TEST_ASSERT((ray_len(server_hs)) >= (2), "two server-side subscriber handles"); int64_t s1 = ((int64_t*)ray_data(server_hs))[0]; int64_t s2 = ((int64_t*)ray_data(server_hs))[1]; -#ifndef RAY_OS_WINDOWS for (int i = 0; i < 2; i++) { ray_selector_t* ssel = ray_poll_get(poll, i == 0 ? s1 : s2); TEST_ASSERT_NOT_NULL(ssel); int sndbuf = 4096; - setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_SNDBUF, (const char*)&sndbuf, sizeof(sndbuf)); } -#endif /* Process default: 64 KiB. h2 alone may hold 4 MiB. */ ray_t* msg = ray_str("(.ipc.txlimit 65536 0)", strlen("(.ipc.txlimit 65536 0)")); @@ -1040,14 +1044,12 @@ static test_result_t test_ipc_outbound_close_hook(void) { TEST_ASSERT_TRUE(pump_until_env_i64_at_least("_oc_out", 7, 1, 1000)); /* B: the peer resets h2 (linger zero, then close → RST) */ -#ifndef RAY_OS_WINDOWS { ray_selector_t* ssel = ray_poll_get(poll, s2); TEST_ASSERT_NOT_NULL(ssel); struct linger lg = { 1, 0 }; - setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); + setsockopt((ray_sock_t)ssel->fd, SOL_SOCKET, SO_LINGER, (const char*)&lg, sizeof(lg)); } -#endif n = snprintf(src, sizeof(src), "(.ipc.close %lld)", (long long)s2); msg = ray_str(src, (size_t)n); cr = ray_ipc_send(hc, msg); @@ -1224,7 +1226,6 @@ static test_result_t test_mcast_sync_reply_after_queued_frame(void) { TEST_ASSERT_FALSE(RAY_IS_ERR(sub)); ray_release(sub); -#ifndef RAY_OS_WINDOWS /* Shrink the server->h1 send buffer so a big frame parks on sel->tx.buf * instead of leaving in a single write. */ ray_t* server_h = ray_env_get(ray_sym_intern("_mc_sub_handle", 14)); @@ -1234,8 +1235,7 @@ static test_result_t test_mcast_sync_reply_after_queued_frame(void) { TEST_ASSERT_NOT_NULL(server_sel); int sndbuf = 4096; setsockopt((ray_sock_t)server_sel->fd, SOL_SOCKET, SO_SNDBUF, - &sndbuf, sizeof(sndbuf)); -#endif + (const char*)&sndbuf, sizeof(sndbuf)); const char* pub_src = "(.mc.pub \"big\" (+ (* (til 100000) 1103515245) 12345))"; diff --git a/test/test_repl.c b/test/test_repl.c index a341cc4f..5397c207 100644 --- a/test/test_repl.c +++ b/test/test_repl.c @@ -88,10 +88,8 @@ extern void* ray_runtime_get_poll(void); * poll first (closes any leftover conns), runtime second. */ static void repl_setup(void) { ray_runtime_create(0, NULL); -#ifndef RAY_OS_WINDOWS ray_poll_t* p = ray_poll_create(); if (p) ray_runtime_set_poll(p); -#endif } static void repl_teardown(void) { @@ -103,7 +101,6 @@ static void repl_teardown(void) { ray_t* args = NULL; ray_release(ray_repl_disconnect_fn(&args, 0)); } -#ifndef RAY_OS_WINDOWS { ray_poll_t* p = (ray_poll_t*)ray_runtime_get_poll(); if (p) { @@ -111,7 +108,6 @@ static void repl_teardown(void) { ray_poll_destroy(p); } } -#endif ray_runtime_destroy(__RUNTIME); } @@ -670,7 +666,9 @@ static test_result_t test_repl_pty_ctrl_d(void) { * code proves it ran while the prompt was idle, and the helper's 5 s * timeout (-2) is what a starved timer would produce. Nothing after * the timer line is ever written to the pty. */ +#ifndef RAY_OS_WINDOWS static int run_pty_listen_with_poll(const char* input); +#endif static test_result_t test_repl_pty_timer_fires_while_idle(void) { #ifndef RAY_OS_WINDOWS int rc = run_pty_listen_with_poll("(.time.timer.set 150 1 (fn [t] (exit 7)))\n"); diff --git a/test/test_runtime.c b/test/test_runtime.c index 35160723..e6bb4f06 100644 --- a/test/test_runtime.c +++ b/test/test_runtime.c @@ -37,8 +37,13 @@ #include #include #include +#ifdef RAY_OS_WINDOWS +#include +#include +#else #include #include +#endif static char* make_tmpdir(void) { char tmpl[] = "/tmp/rayforce-rt-test-XXXXXX"; @@ -70,6 +75,11 @@ static test_result_t test_create_with_sym_absent_is_ok(void) { * passing a path whose parent exists but isn't a directory (ENOTDIR) — * portable across Linux/macOS without needing root or chmod games. */ static test_result_t test_create_with_sym_io_error_surfaces(void) { +#if defined(_WIN32) + /* Win32 reports a path through a regular file as "path not found" + * (ENOENT, the missing-file case), never ENOTDIR. */ + SKIP("no ENOTDIR on Windows"); +#endif char* dir = make_tmpdir(); TEST_ASSERT_NOT_NULL(dir); @@ -1265,9 +1275,7 @@ static test_result_t test_syscov_splayed_set_with_sym_path(void) { } /* cleanup — no free(dir), it's a stack pointer */ - char cmd[512]; - snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); - system(cmd); + (void)ray_test_rm_rf(dir); PASS(); } diff --git a/test/test_splay.c b/test/test_splay.c index 1137cbbd..34188b46 100644 --- a/test/test_splay.c +++ b/test/test_splay.c @@ -65,9 +65,7 @@ static void splay_teardown(void) { /* Remove temp dir tree */ static void rm_rf(const char* path) { - char cmd[512]; - snprintf(cmd, sizeof(cmd), "rm -rf %s", path); - (void)!system(cmd); + (void)ray_test_rm_rf(path); } /* ========================================================================= @@ -181,9 +179,7 @@ static test_result_t test_load_missing_schema(void) { /* Directory exists but contains no .d file */ const char* dir = TMP_SPLAY_BASE "/no_schema"; rm_rf(dir); - char cmd[512]; - snprintf(cmd, sizeof(cmd), "mkdir -p %s", dir); - (void)!system(cmd); + (void)ray_test_mkdir_p(dir); ray_t* r = ray_splay_load(dir, NULL); /* ray_col_load of missing file returns an error object */ @@ -499,9 +495,7 @@ static test_result_t test_save_sym_error(void) { char sym_as_dir[512]; snprintf(sym_as_dir, sizeof(sym_as_dir), "%s/sym_dir", dir); /* Ensure parent dir exists first */ - char mk[600]; - snprintf(mk, sizeof(mk), "mkdir -p %s", sym_as_dir); - (void)!system(mk); + (void)ray_test_mkdir_p(sym_as_dir); ray_err_t err = ray_splay_save(tbl, dir, sym_as_dir); /* Either succeeds (some impls tolerate it) or returns an error — either @@ -732,8 +726,8 @@ static test_result_t test_save_bulk_with_sym_path(void) { * 19. splay_save_impl: snprintf overflow for the column / ".d" paths. * Requires strlen(dir) >= 1021 so that strlen(dir)+3 >= 1024. * Build a deeply nested path using short components (≤ 50 chars each) - * so the filesystem NAME_MAX (255) is not exceeded, then call mkdir_p - * via system(), then ray_splay_save → snprintf("%s/.d") fires range. + * so the filesystem NAME_MAX (255) is not exceeded, then create it + * with ray_test_mkdir_p, then ray_splay_save → snprintf("%s/.d") fires range. * * Path layout (each component 50 chars): * /tmp/rft_deep_save/ (18 chars) @@ -748,6 +742,10 @@ static test_result_t test_save_dir_path_too_long(void) { * fires under the same condition on Linux PATH_MAX = 4096. Skip * on Darwin — the Linux runner covers the regression. */ SKIP("PATH_MAX=1024 on macOS — deep-mkdir fixture not portable"); +#elif defined(_WIN32) + /* Win32 directory paths stop at MAX_PATH (~260) without long-path + * opt-in, far short of the 1021-char tree. */ + SKIP("MAX_PATH=260 on Windows — deep-mkdir fixture not portable"); #endif /* Construct the nested path in a buffer */ char long_dir[2048]; @@ -771,10 +769,8 @@ static test_result_t test_save_dir_path_too_long(void) { TEST_ASSERT_TRUE((size_t)off >= 1021); /* Create the directory tree so ray_mkdir_p inside save succeeds. - * system("mkdir -p ...") handles arbitrarily deep paths. */ - char mk[4096]; - snprintf(mk, sizeof(mk), "mkdir -p \"%s\"", long_dir); - (void)!system(mk); + * ray_test_mkdir_p handles arbitrarily deep paths. */ + (void)ray_test_mkdir_p(long_dir); int64_t id_v2 = ray_sym_intern("v2long", 6); int64_t raw[] = {1}; @@ -794,9 +790,7 @@ static test_result_t test_save_dir_path_too_long(void) { ray_release(col); ray_release(tbl); /* Cleanup entire nested tree from the base */ - char rm_cmd[256]; - snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf /tmp/rft_deep_save"); - (void)!system(rm_cmd); + (void)ray_test_rm_rf("/tmp/rft_deep_save"); PASS(); } @@ -884,9 +878,7 @@ static test_result_t test_trace_missing_schema(void) { const char* dir = TMP_SPLAY_BASE "/trace_noschema"; rm_rf(dir); /* Create dir without .d file */ - char mk[512]; - snprintf(mk, sizeof(mk), "mkdir -p %s", dir); - (void)!system(mk); + (void)ray_test_mkdir_p(dir); setenv("RAY_CSV_TRACE", "1", 1); ray_t* r = ray_splay_load(dir, NULL); @@ -985,11 +977,14 @@ static test_result_t test_trace_fresh_load(void) { * (with .d last, the column save is the first write to hit the dir). * ========================================================================= */ static test_result_t test_save_schema_write_fails(void) { +#if defined(_WIN32) + /* chmod cannot make a Windows directory refuse new files: the + * read-only attribute is ignored for directories. */ + SKIP("read-only directories are not enforced on Windows"); +#endif const char* dir = TMP_SPLAY_BASE "/no_write_schema"; rm_rf(dir); - char mk[512]; - snprintf(mk, sizeof(mk), "mkdir -p %s", dir); - (void)!system(mk); + (void)ray_test_mkdir_p(dir); /* Make dir read-only so .d cannot be written */ chmod(dir, 0555); diff --git a/test/test_store.c b/test/test_store.c index f4ca1056..1e7d849a 100644 --- a/test/test_store.c +++ b/test/test_store.c @@ -326,7 +326,7 @@ static test_result_t test_col_mmap_nofile(void) { static test_result_t test_splay_open_roundtrip(void) { /* Clean up any leftover splay dir */ - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); /* Build a 3-column table: I64, F64, I32 */ ray_t* tbl = ray_table_new(4); @@ -398,14 +398,14 @@ static test_result_t test_splay_open_roundtrip(void) { ray_release(tbl); /* Cleanup */ - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* ---- test_splay_str_column_roundtrip ----------------------------------- */ static test_result_t test_splay_str_column_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); ray_t* tbl = ray_table_new(2); TEST_ASSERT_NOT_NULL(tbl); @@ -471,7 +471,7 @@ static test_result_t test_splay_str_column_roundtrip(void) { ray_release(names); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } @@ -485,7 +485,7 @@ static test_result_t test_splay_str_column_roundtrip(void) { * ---------------------------------------------------------------------- */ static test_result_t test_splay_short_strv_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); int64_t id_short = ray_sym_intern("short", 5); int64_t id_empty = ray_sym_intern("empty", 5); @@ -547,13 +547,13 @@ static test_result_t test_splay_short_strv_roundtrip(void) { ray_release(tbl); ray_release(tbl2); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* ---- test_splay_dict_column_roundtrip --------------------------------- */ static test_result_t test_splay_dict_column_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); int64_t ids_raw[] = {1, 2}; ray_t* ids = ray_vec_from_raw(RAY_I64, ids_raw, 2); @@ -608,13 +608,13 @@ static test_result_t test_splay_dict_column_roundtrip(void) { ray_release(tbl); ray_release(ids); ray_release(sched); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* ---- test_splay_empty_list_column_roundtrip --------------------------- */ static test_result_t test_splay_empty_list_column_roundtrip(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); ray_t* ids = ray_vec_new(RAY_I64, 0); ray_t* who = ray_vec_new(RAY_SYM, 0); @@ -657,14 +657,14 @@ static test_result_t test_splay_empty_list_column_roundtrip(void) { ray_release(ids); ray_release(who); ray_release(sched); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } /* A deterministic unsupported column must be rejected before an earlier * column can replace the committed generation. */ static test_result_t test_splay_save_preflight_preserves_generation(void) { - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); int64_t k_id = ray_sym_intern("k", 1); int64_t v_id = ray_sym_intern("v", 1); @@ -705,7 +705,7 @@ static test_result_t test_splay_save_preflight_preserves_generation(void) { ray_release(good); ray_release(old_v); ray_release(old_k); - (void)!system("rm -rf " TMP_SPLAY_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_DIR); PASS(); } @@ -838,9 +838,9 @@ static test_result_t test_parted_release(void) { static test_result_t test_part_open(void) { /* Setup: create a 2-partition db with 2 columns each */ - (void)!system("rm -rf " TMP_PART_DB); - (void)!system("mkdir -p " TMP_PART_DB "/2024.01.01/" TMP_TABLE_NAME); - (void)!system("mkdir -p " TMP_PART_DB "/2024.01.02/" TMP_TABLE_NAME); + (void)ray_test_rm_rf(TMP_PART_DB); + (void)ray_test_mkdir_p(TMP_PART_DB "/2024.01.01/" TMP_TABLE_NAME); + (void)ray_test_mkdir_p(TMP_PART_DB "/2024.01.02/" TMP_TABLE_NAME); /* Partition 1: 3 rows */ int64_t raw_a1[] = {10, 20, 30}; @@ -941,7 +941,7 @@ static test_result_t test_part_open(void) { /* Release — should unmap all segments */ ray_release(parted); - (void)!system("rm -rf " TMP_PART_DB); + (void)ray_test_rm_rf(TMP_PART_DB); PASS(); } @@ -949,7 +949,7 @@ static test_result_t test_part_open(void) { /* ray_parted_tables lists the splayed-table subdirectories of the first * partition as a sorted SYM vector usable with ray_read_parted. */ static test_result_t test_parted_tables(void) { - (void)!system("rm -rf " TMP_PART_DB); + (void)ray_test_rm_rf(TMP_PART_DB); /* Two tables (trades, quotes) across two partitions. */ const char* dirs[] = { TMP_PART_DB "/2024.01.01/trades", TMP_PART_DB "/2024.01.01/quotes", @@ -986,16 +986,17 @@ static test_result_t test_parted_tables(void) { /* An existing-but-empty root (no partition dirs) lists no tables — * an empty SYM vector, not an error. */ - (void)!system("rm -rf " TMP_PART_DB "_np && mkdir -p " TMP_PART_DB "_np"); + (void)ray_test_rm_rf(TMP_PART_DB "_np"); + (void)ray_test_mkdir_p(TMP_PART_DB "_np"); ray_t* empty = ray_parted_tables(TMP_PART_DB "_np"); TEST_ASSERT_NOT_NULL(empty); TEST_ASSERT_FALSE(RAY_IS_ERR(empty)); TEST_ASSERT_EQ_I(empty->type, RAY_SYM); TEST_ASSERT_EQ_I(empty->len, 0); ray_release(empty); - (void)!system("rm -rf " TMP_PART_DB "_np"); + (void)ray_test_rm_rf(TMP_PART_DB "_np"); - (void)!system("rm -rf " TMP_PART_DB); + (void)ray_test_rm_rf(TMP_PART_DB); PASS(); } @@ -1662,7 +1663,7 @@ static test_result_t test_sym_col_valid_roundtrip(void) { #define TMP_SYM_PATH "/tmp/rayforce_test_splay_sym_file" static test_result_t test_splay_load_with_sym(void) { - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); /* Intern symbols and build a table with a RAY_SYM column */ @@ -1711,7 +1712,7 @@ static test_result_t test_splay_load_with_sym(void) { ray_release(col_name); ray_release(col_age); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); unlink(TMP_SYM_PATH ".lk"); PASS(); @@ -1720,7 +1721,7 @@ static test_result_t test_splay_load_with_sym(void) { /* ---- test_splay_load_sym_missing_corrupt ------------------------------- */ static test_result_t test_splay_load_sym_missing_corrupt(void) { - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); /* Intern symbols and build a table with a RAY_SYM column */ @@ -1757,7 +1758,7 @@ static test_result_t test_splay_load_sym_missing_corrupt(void) { ray_release(col); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); unlink(TMP_SYM_PATH); unlink(TMP_SYM_PATH ".lk"); PASS(); @@ -1766,7 +1767,7 @@ static test_result_t test_splay_load_sym_missing_corrupt(void) { /* ---- test_read_splayed_bad_sym_fatal ----------------------------------- */ static test_result_t test_read_splayed_bad_sym_fatal(void) { - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); /* Build a simple table (no RAY_SYM columns needed) */ int64_t id_x = ray_sym_intern("x", 1); @@ -1793,7 +1794,7 @@ static test_result_t test_read_splayed_bad_sym_fatal(void) { ray_release(col_x); ray_release(tbl); - (void)!system("rm -rf " TMP_SPLAY_SYM_DIR); + (void)ray_test_rm_rf(TMP_SPLAY_SYM_DIR); PASS(); } diff --git a/test/test_traverse.c b/test/test_traverse.c index db72da8d..d976d0d4 100644 --- a/test/test_traverse.c +++ b/test/test_traverse.c @@ -33,7 +33,7 @@ #include #include #include -#ifndef __SANITIZE_ADDRESS__ +#if !defined(__SANITIZE_ADDRESS__) && !defined(_WIN32) /* setrlimit: POSIX only */ #include #endif @@ -3904,7 +3904,7 @@ static test_result_t test_k_shortest_found_path_dup(void) { /* -------------------------------------------------------------------------- * Helper: read VmSize from /proc/self/status; returns 0 on failure. * -------------------------------------------------------------------------- */ -#ifndef __SANITIZE_ADDRESS__ +#if !defined(__SANITIZE_ADDRESS__) && !defined(_WIN32) #include static size_t get_vmsize_bytes(void) { FILE* f = fopen("/proc/self/status", "r"); @@ -4241,7 +4241,7 @@ static test_result_t test_traverse_oom_paths(void) { ray_heap_destroy(); PASS(); } -#endif /* __SANITIZE_ADDRESS__ */ +#endif /* !__SANITIZE_ADDRESS__ && !_WIN32 */ /* -------------------------------------------------------------------------- * Test: exec_expand with SIP bitmap build where rev.n_nodes > fwd.n_nodes. @@ -5725,7 +5725,7 @@ const test_entry_t traverse_entries[] = { { "traverse/k_shortest_large_k", test_k_shortest_large_k, NULL, NULL }, { "traverse/betweenness_with_rev_edges", test_betweenness_with_rev_edges, NULL, NULL }, { "traverse/closeness_sampled_norm", test_closeness_sampled_norm, NULL, NULL }, -#ifndef __SANITIZE_ADDRESS__ +#if !defined(__SANITIZE_ADDRESS__) && !defined(_WIN32) { "traverse/traverse_oom_paths", test_traverse_oom_paths, NULL, NULL }, #endif { "traverse/shortest_path_exceeds_254", test_shortest_path_exceeds_254, NULL, NULL }, From fbf55cf8baaceea02c68eec4591fb7535de5e407 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Tue, 22 Sep 2026 14:58:57 +0300 Subject: [PATCH 07/13] docs: Windows build instructions and platform status Co-Authored-By: Claude Opus 5 (1M context) --- CONTRIBUTING.md | 22 ++++++++++++++++++++++ RELEASE.md | 8 ++++---- docs/docs/guides/memory.md | 4 ---- docs/docs/index.md | 2 +- docs/docs/namespaces/sys.md | 2 +- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aea56e49..cbbaf68d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,28 @@ a focused subset with: ./rayforce.test -f ``` +A `.rfl` file that needs a POSIX shell or filesystem (fixtures built or +checked through `.sys.exec`, `/proc`, `/dev/tcp`, …) carries the line +`;; @requires: posix`; on Windows the runner reports it as `SKIP` rather +than running it. C tests use `#ifndef RAY_OS_WINDOWS` / `SKIP(...)` for the +same purpose. Prefer the shell-free helpers `ray_test_rm_rf` / +`ray_test_mkdir_p` (`test/test.h`) over `system("rm -rf …")` in C tests. + +### Windows + +Build with the MSYS2 CLANG64 (or MINGW64) toolchain — `pacman -S +mingw-w64-clang-x86_64-clang make` — from an MSYS2 shell or with +`C:\msys64\clang64\bin` and `C:\msys64\usr\bin` on `PATH`: + +```sh +make # debug build (ASan + UBSan) +make test +make release +``` + +The debug binaries load the ASan runtime DLL from `clang64\bin`, so keep it on +`PATH` when running them. + ## Stability tooling Beyond the ASan/UBSan test run, the repo carries a stability toolset. These diff --git a/RELEASE.md b/RELEASE.md index fb141e6e..76421ebe 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -136,7 +136,7 @@ Each release publishes, in addition to the source: ## Platform support -Linux and macOS binaries are published today. Windows is not build-ready yet -(IOCP backend is a stub, `main.c`/`heap.c` have unguarded POSIX calls, and the -Makefile has no Windows toolchain path); once ported, add a `windows-latest` row -to the `build` matrix in `.github/workflows/release.yml`. +Linux and macOS binaries are published today. Windows builds and passes the +test suite from source with the MSYS2 CLANG64 toolchain (see CONTRIBUTING.md), +but no Windows binary is published yet; to ship one, add a `windows-latest` +row (MSYS2 CLANG64) to the `build` matrix in `.github/workflows/release.yml`. diff --git a/docs/docs/guides/memory.md b/docs/docs/guides/memory.md index 998c1d2d..a0050195 100644 --- a/docs/docs/guides/memory.md +++ b/docs/docs/guides/memory.md @@ -104,10 +104,6 @@ total-mem | 16777216000 | `page-size` | OS page size in bytes | | `total-mem` | Total physical RAM in bytes | -!!! note "Note" - - On Windows, only `cores` is currently reported. - ## 5. Progress Monitoring Long-running queries display a progress bar automatically in the REPL. The bar appears after approximately 2 seconds of execution and shows real-time feedback. diff --git a/docs/docs/index.md b/docs/docs/index.md index 6550f46a..162253e7 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -2,7 +2,7 @@ Embeddable columnar analytics and graph traversal engine in pure C. -Rayforce combines morsel-driven vectorized execution, a multi-pass query optimizer, and a native CSR graph engine in a single pipeline. It is queried through the **Rayfall** language, exposes a C API for embedding, and runs on Linux and macOS (Windows is planned — the IOCP backend is still a stub). +Rayforce combines morsel-driven vectorized execution, a multi-pass query optimizer, and a native CSR graph engine in a single pipeline. It is queried through the **Rayfall** language, exposes a C API for embedding, and runs on Linux and macOS, and builds on Windows from source (MSYS2 CLANG64). [Quick Start](getting-started/quick-start.md){ .md-button .md-button--primary } [Functions Reference](language/functions.md){ .md-button } diff --git a/docs/docs/namespaces/sys.md b/docs/docs/namespaces/sys.md index 5e13d189..f6eefbff 100644 --- a/docs/docs/namespaces/sys.md +++ b/docs/docs/namespaces/sys.md @@ -75,7 +75,7 @@ Signature: `(.sys.build)`. Returns a dict with `version` (string) and `build-dat ## `.sys.info` { #sys-info } -Signature: `(.sys.info)`. Returns `{cores: i64, page-size: i64, total-mem: i64, pid: i64, hostname: str}` on POSIX. On Windows the machine facts fall back to `{cores: 1}` (the sysconf-backed values aren't wired), but `pid` and `hostname` are answered on both platforms. +Signature: `(.sys.info)`. Returns `{cores: i64, page-size: i64, total-mem: i64, pid: i64, hostname: str}` on every platform (on Windows from `GetSystemInfo` / `GlobalMemoryStatusEx`). ```lisp (.sys.info) From cfdbb34ed95a25789d173ef7687ad5aad2f021e3 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Wed, 23 Sep 2026 13:23:54 +0300 Subject: [PATCH 08/13] fix: readable REPL prompt and CPU name in the banner on Windows No console font Windows ships has U+2023 (checked Consolas, Cascadia Mono, Lucida Console, Courier New) and the classic console does no font fallback, so the prompt rendered as '?'. Use the nearest filled triangle they all do have, U+25BA, which is also three UTF-8 bytes. The banner's CPU line said 'unknown': read ProcessorNameString instead. Co-Authored-By: Claude Opus 5 (1M context) --- docs/docs/getting-started/quick-start.md | 2 +- src/app/repl.c | 13 ++++++++++++- src/app/term.c | 12 +++++++++++- test/test_term.c | 7 ++++++- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/docs/getting-started/quick-start.md b/docs/docs/getting-started/quick-start.md index 73a2dc04..143da205 100644 --- a/docs/docs/getting-started/quick-start.md +++ b/docs/docs/getting-started/quick-start.md @@ -58,7 +58,7 @@ The Rayfall REPL provides an interactive environment with syntax highlighting, b ./rayforce ``` -You will see the `‣` prompt (a green triangle bullet): +You will see the `‣` prompt (a green triangle bullet; `►` on Windows, whose console fonts have no `‣`): ```text ‣ diff --git a/src/app/repl.c b/src/app/repl.c index 7036c63c..b5cd32d8 100644 --- a/src/app/repl.c +++ b/src/app/repl.c @@ -353,7 +353,18 @@ static void get_cpu_name(char* buf, size_t sz) { if (sysctlbyname("machdep.cpu.brand_string", buf, &len, NULL, 0) != 0) snprintf(buf, sz, "unknown"); #elif defined(RAY_OS_WINDOWS) - snprintf(buf, sz, "unknown"); + /* The brand string the firmware reported, same text as /proc/cpuinfo's + * "model name"; it is padded with trailing spaces, so trim them. */ + DWORD n = (DWORD)sz; + if (RegGetValueA(HKEY_LOCAL_MACHINE, + "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", + "ProcessorNameString", RRF_RT_REG_SZ, NULL, + buf, &n) == ERROR_SUCCESS) { + size_t len = strlen(buf); + while (len > 0 && buf[len - 1] == ' ') buf[--len] = '\0'; + } else { + snprintf(buf, sz, "unknown"); + } #else snprintf(buf, sz, "unknown"); #endif diff --git a/src/app/term.c b/src/app/term.c index 79509f18..3b1560a6 100644 --- a/src/app/term.c +++ b/src/app/term.c @@ -1517,8 +1517,18 @@ int32_t ray_term_count_unmatched(ray_term_t* term) { /* ===== Prompt ===== */ -/* Green ‣ (U+2023) prompt, matching Rayforce style */ +/* Green ‣ (U+2023) prompt, matching Rayforce style. + * + * No console font Windows ships has U+2023 — not Consolas, Cascadia Mono, + * Lucida Console or Courier New — and the classic console does no font + * fallback, so the prompt renders as "?" there. Windows uses ► (U+25BA), + * the nearest filled triangle all four of them do have; it is also three + * UTF-8 bytes, so the byte and visual widths below are unchanged. */ +#if defined(RAY_OS_WINDOWS) +#define PROMPT_STR "\033[32m\xe2\x96\xba\033[0m " +#else #define PROMPT_STR "\033[32m\xe2\x80\xa3\033[0m " +#endif #define PROMPT_LEN 13 /* ESC[32m (5) + ‣ (3) + ESC[0m (4) + space (1) = 13 bytes */ #define PROMPT_VIS 2 /* visual: ‣ + space */ #define CONT_PROMPT_STR "\033[90m\xe2\x80\xa6\033[0m " /* gray … (U+2026) */ diff --git a/test/test_term.c b/test/test_term.c index f29ddc08..319c2f98 100644 --- a/test/test_term.c +++ b/test/test_term.c @@ -1185,7 +1185,12 @@ static test_result_t test_term_prompt_emits_bytes(void) { ray_term_prompt(t); fflush(stdout); int32_t n = capture_end(saved, path, cap, sizeof cap); - int saw_arrow = strstr(cap, "\xe2\x80\xa3") != NULL; /* ‣ */ + /* ‣ (U+2023), or ► (U+25BA) where no console font has ‣ — see term.c */ +#if defined(RAY_OS_WINDOWS) + int saw_arrow = strstr(cap, "\xe2\x96\xba") != NULL; +#else + int saw_arrow = strstr(cap, "\xe2\x80\xa3") != NULL; +#endif int saw_green = strstr(cap, "\033[32m") != NULL; TEST_ASSERT_FMT(n > 0, "no prompt output"); TEST_ASSERT_FMT(saw_arrow, "missing ‣ arrow in prompt: bytes=%d", n); From bb6b5d8875ad6eb4ed3b8b57d15e3b0e8771019b Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Wed, 23 Sep 2026 13:32:14 +0300 Subject: [PATCH 09/13] bench: build the micro-benchmarks on Windows, record a Windows/Linux check alloc and agg_v2 read peak RSS through getrusage; use GetProcessMemoryInfo there. windows_vs_linux.md records the numbers the port was checked against. Co-Authored-By: Claude Opus 5 (1M context) --- bench/agg_v2/main.c | 14 ++++++- bench/alloc/main.c | 14 ++++++- bench/bottleneck/windows_vs_linux.md | 56 ++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 bench/bottleneck/windows_vs_linux.md diff --git a/bench/agg_v2/main.c b/bench/agg_v2/main.c index 401365ce..908fc3f4 100644 --- a/bench/agg_v2/main.c +++ b/bench/agg_v2/main.c @@ -46,7 +46,13 @@ #include #include #include -#include +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +# include +#else +# include +#endif /* ---------- timing ---------- */ static double now_ms(void) { @@ -69,12 +75,18 @@ static double vmin(const double* arr, int n) { return m; } static long max_rss_kb(void) { +#if defined(_WIN32) + PROCESS_MEMORY_COUNTERS pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) return 0; + return (long)(pmc.PeakWorkingSetSize / 1024); +#else struct rusage ru; getrusage(RUSAGE_SELF, &ru); #if defined(__APPLE__) return ru.ru_maxrss / 1024; #else return ru.ru_maxrss; #endif +#endif } /* ---------- deterministic PRNG (splitmix64) ---------- */ diff --git a/bench/alloc/main.c b/bench/alloc/main.c index 9bbefc12..3294e918 100644 --- a/bench/alloc/main.c +++ b/bench/alloc/main.c @@ -11,7 +11,13 @@ #include #include #include -#include +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +# include +#else +# include +#endif static double now_s(void) { struct timespec ts; @@ -66,6 +72,11 @@ static void* consumer(void* _) { } static long max_rss_kb(void) { +#if defined(_WIN32) + PROCESS_MEMORY_COUNTERS pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) return 0; + return (long)(pmc.PeakWorkingSetSize / 1024); +#else struct rusage ru; getrusage(RUSAGE_SELF, &ru); /* Linux: ru_maxrss is KB; macOS: bytes. Normalize to KB. */ #if defined(__APPLE__) @@ -73,6 +84,7 @@ static long max_rss_kb(void) { #else return ru.ru_maxrss; #endif +#endif } int main(void) { diff --git a/bench/bottleneck/windows_vs_linux.md b/bench/bottleneck/windows_vs_linux.md new file mode 100644 index 00000000..92874cf8 --- /dev/null +++ b/bench/bottleneck/windows_vs_linux.md @@ -0,0 +1,56 @@ +# Windows vs Linux sanity check + +Not a performance study: a coarse check that the Windows port lands in the +same ballpark as Linux, run while porting (`serhii/windows-port`). The two +sides do not share a compiler, an allocator-visible kernel, or a filesystem, +so only order-of-magnitude gaps are meaningful here. + +## Environment + +**CPU**: 11th Gen Intel Core i7 (8 logical cores) — one laptop, both runs +**Windows**: Windows 11, clang 20.1.8 (MSYS2 CLANG64), 32 GiB +**Linux**: WSL2 (kernel 6.6.87.2-microsoft-standard-WSL2, Ubuntu 24.04), gcc 13.3.0, 15 GiB to the VM +**Build**: `make release` both sides (`-O3 -march=native`, no sanitizers — `nm bench-alloc | grep -ci asan` → 0) + +WSL2 is a virtual machine with its own memory budget and a virtualised +filesystem; that alone moves I/O and page-fault numbers. Treat the file-backed +rows as indicative only. + +## Allocator micro-benchmark (`bench/alloc`) + +| case | Windows | Linux | +|------|---------|-------| +| atom-64B | 85.7 Mops/s | 80.9 Mops/s | +| vec-256B | 85.5 Mops/s | 78.6 Mops/s | +| morsel-8K | 88.2 Mops/s | 80.3 Mops/s | +| morsel-16K | 88.4 Mops/s | 78.2 Mops/s | +| large-1M | 62.7 Mops/s | 56.4 Mops/s | +| producer-consumer | 9.2 Mops/s, peak RSS 28 MB | 6.0 Mops/s, peak RSS 68 MB | + +## Engine operations (5M rows, `timeit`, median of 3) + +| operation | Windows (ms) | Linux (ms) | Win/Lin | +|-----------|-------------:|-----------:|--------:| +| arith-f64 (`sum (* f 1.5)`) | 7.94 | 5.10 | 1.56 | +| sort-i64 | 9.89 | 8.78 | 1.13 | +| distinct-i64 | 2.96 | 4.69 | 0.63 | +| group-by sym | 3.44 | 3.93 | 0.88 | +| select where | 7.13 | 14.14 | 0.50 | +| inner-join | 108.6 | 108.3 | 1.00 | +| csv write (5M rows) | 2201 | 1654 | 1.33 | +| csv read | 155 | 186 | 0.83 | +| splayed set | 509 | 580 | 0.88 | +| splayed get + count | 5.9 | 0.28 | 21 | + +`sum`/`avg` are omitted: both platforms report ~0.005 ms, the DAG elides them. + +## Reading + +Compute paths agree within a factor of ~1.6 either way, which is compiler and +noise territory, and the allocator is slightly ahead on Windows. + +The one real gap is `splayed get + count` (5.9 ms vs 0.28 ms). It opens and +maps one file per column, so it measures file-open cost, not the engine: +`CreateFileA` + `CreateFileMapping` + `MapViewOfFile` per column, plus +whatever on-access scanning is installed. The absolute cost is small and it is +paid per table open, but a wide table opened in a loop would feel it. From fb392d92cb104f58904bbce23b5594f5f5612599 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Wed, 23 Sep 2026 14:11:18 +0300 Subject: [PATCH 10/13] bench: record the recent perf benches on Windows vs Linux Co-Authored-By: Claude Opus 5 (1M context) --- bench/bottleneck/windows_vs_linux.md | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/bench/bottleneck/windows_vs_linux.md b/bench/bottleneck/windows_vs_linux.md index 92874cf8..c7b0e30a 100644 --- a/bench/bottleneck/windows_vs_linux.md +++ b/bench/bottleneck/windows_vs_linux.md @@ -54,3 +54,39 @@ maps one file per column, so it measures file-open cost, not the engine: `CreateFileA` + `CreateFileMapping` + `MapViewOfFile` per column, plus whatever on-access scanning is installed. The absolute cost is small and it is paid per table open, but a wide table opened in a loop would feel it. + +## The benches recent PRs shipped + +Same binaries, built from the release objects on each side. + +**`bench/join_nullfree` (#598, null-free key fast path).** The optimisation +engages on Windows — the `nullfree` counter advances on the null-free cases +and stays put on the nullable one, as on Linux. + +| case | Windows median (baseline → fast) | Linux median | +|------|---------------------------------|--------------| +| SYM2 | 355.3 → 336.0 ms (-5.4%) | 413.6 → 408.4 ms (-1.3%) | +| SYM2-NULL (must not fire) | 348.2 → 349.8 ms (+0.5%) | 407.1 → 412.9 ms (+1.4%) | +| I64 | 102.7 → 89.2 ms (-13.2%) | 100.9 → 96.3 ms (-4.6%) | + +**`bench/join_dup` (duplicate-key fallback).** The pathological case is fixed +on both: CATASTROPHIC-INNER post-fix ~170 ms on Windows and ~230 ms on Linux, +against ~2.6 s pre-fix (Windows) — the same order-of-magnitude win. + +**`bench/join_buildside` (build-side swap).** The swap fires on Windows and +pays off by the same factor: MANY-TO-MANY 207 ms swapped vs 494 ms legacy +(2.4x); Linux 188 vs 431 (2.3x). HEAVY-DUP-WIN: 1.7 s vs 6.2 s (Windows), +1.8 s vs 8.2 s (Linux). + +**`bench/idx_route` Q3** (1000 lookups/rep): indexed 0.014 ms/batch on +Windows, 0.009 on Linux; the unindexed control is 0.004 on both. + +Not runnable as-is: + +- `bench/group_pushdown` and `bench/agg_v2` no longer compile **on either + platform** — they use `ray_op.inputs` and `ray_group2/3`, which the engine + no longer has. Pre-existing, unrelated to the port. +- `bench/groupby_shapes/*.py` needs python3, which a stock Windows lacks; the + `.rfl` cases in that directory run directly under `rayforce` on both. +- `scripts/soak.sh` and `scripts/fuzz-seed-*.sh` are bash and stay POSIX-only + (the fuzzing runtime is Linux-only anyway, see the Makefile). From 55430068b15aab977ae0b74b1703c7c136be4676 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Wed, 23 Sep 2026 14:43:41 +0300 Subject: [PATCH 11/13] fix(format): render i64 at full width, not through 32-bit long MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interpolation (format/println), print and the pivot / column-name helpers formatted an i64 with "%ld" and a (long) cast. long is 64-bit on LP64 and 32-bit on Windows, so there 10^18 printed as -1486618624 and a pivot keyed on values above 2^31 produced truncated column NAMES — a data defect, not only a display one. Use PRId64 throughout; regression test included. Co-Authored-By: Claude Opus 5 (1M context) --- src/ops/builtins.c | 8 ++++---- src/ops/pivot.c | 5 +++-- src/ops/tblop.c | 4 ++-- test/rfl/regress/i64_text_width.rfl | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 test/rfl/regress/i64_text_width.rfl diff --git a/src/ops/builtins.c b/src/ops/builtins.c index 18dd8d35..9b2fb31b 100644 --- a/src/ops/builtins.c +++ b/src/ops/builtins.c @@ -110,7 +110,7 @@ void ray_lang_print(FILE* fp, ray_t* val) { return; } switch (val->type) { - case -RAY_I64: fprintf(fp, "%ld", (long)val->i64); break; + case -RAY_I64: fprintf(fp, "%" PRId64, val->i64); break; case -RAY_F64: { double fv = val->f64; fv = clear_neg_zero(fv); @@ -142,8 +142,8 @@ void ray_lang_print(FILE* fp, ray_t* val) { break; } case RAY_TABLE: - fprintf(fp, "", - (long)ray_table_nrows(val), (long)ray_table_ncols(val)); + fprintf(fp, "
", + ray_table_nrows(val), ray_table_ncols(val)); break; case RAY_UNARY: case RAY_BINARY: case RAY_VARY: { const char* name = ray_fn_name(val); @@ -204,7 +204,7 @@ static char* fmt_interpolate(const char* fmt, size_t flen, ray_t** args, int64_t RAY_ATOM_IS_NULL(a)) { tlen = snprintf(tmp, sizeof(tmp), "%s", null_literal_str(a->type)); } else if (a->type == -RAY_I64) { - tlen = snprintf(tmp, sizeof(tmp), "%ld", (long)a->i64); + tlen = snprintf(tmp, sizeof(tmp), "%" PRId64, a->i64); } else if (a->type == -RAY_F64) { double fv = a->f64; fv = clear_neg_zero(fv); diff --git a/src/ops/pivot.c b/src/ops/pivot.c index f3b6eb52..0be4a557 100644 --- a/src/ops/pivot.c +++ b/src/ops/pivot.c @@ -21,6 +21,7 @@ * SOFTWARE. */ +#include #include "ops/internal.h" #include "ops/hash.h" #include "ops/idxop.h" @@ -1910,9 +1911,9 @@ ray_t* exec_pivot(ray_graph_t* g, ray_op_t* op, ray_t* tbl) { len = snprintf(buf, sizeof(buf), "%s", pval ? "true" : "false"); } else if (pt == RAY_I64 || pt == RAY_I32 || pt == RAY_I16 || pt == RAY_DATE || pt == RAY_TIME || pt == RAY_TIMESTAMP) { - len = snprintf(buf, sizeof(buf), "%ld", (long)pval); + len = snprintf(buf, sizeof(buf), "%" PRId64, pval); } else { - len = snprintf(buf, sizeof(buf), "col%ld", (long)pval); + len = snprintf(buf, sizeof(buf), "col%" PRId64, pval); } col_sym = ray_sym_intern(buf, (size_t)len); } diff --git a/src/ops/tblop.c b/src/ops/tblop.c index 7da33443..375d4ae3 100644 --- a/src/ops/tblop.c +++ b/src/ops/tblop.c @@ -627,7 +627,7 @@ static ray_t* pivot_fn_impl(ray_t* tbl, ray_t* index_arg, ray_t* pivot_col_name, if (pval->type == -RAY_SYM) { col_sym = pval->i64; } else if (pval->type == -RAY_I64) { - char buf[64]; int len = snprintf(buf, sizeof(buf), "%ld", (long)pval->i64); + char buf[64]; int len = snprintf(buf, sizeof(buf), "%" PRId64, pval->i64); col_sym = ray_sym_intern(buf, (size_t)len); } else if (pval->type == -RAY_F64) { double fv = clear_neg_zero(pval->f64); @@ -636,7 +636,7 @@ static ray_t* pivot_fn_impl(ray_t* tbl, ray_t* index_arg, ray_t* pivot_col_name, } else if (pval->type == -RAY_BOOL) { col_sym = ray_sym_intern(pval->b8 ? "true" : "false", pval->b8 ? 4 : 5); } else { - char buf[64]; int len = snprintf(buf, sizeof(buf), "col%ld", (long)pval->i64); + char buf[64]; int len = snprintf(buf, sizeof(buf), "col%" PRId64, pval->i64); col_sym = ray_sym_intern(buf, (size_t)len); } if (a1) ray_release(pval); diff --git a/test/rfl/regress/i64_text_width.rfl b/test/rfl/regress/i64_text_width.rfl new file mode 100644 index 00000000..8ae3affc --- /dev/null +++ b/test/rfl/regress/i64_text_width.rfl @@ -0,0 +1,18 @@ +;; An i64 must survive every text path in full 64-bit width. The +;; interpolation, print and pivot/column-name helpers used to render it with +;; "%ld" + (long), which is 32-bit on Windows (LLP64): 10^18 came back as +;; -1486618624, and a pivot keyed on large integers produced wrong column +;; names — a data defect, not just a display one. + +(format "%" 1000000000000000000) -- "1000000000000000000" +(format "%" 9223372036854775807) -- "9223372036854775807" +(format "%" -9223372036854775806) -- "-9223372036854775806" +(format "%" (count (til 3000000000))) -- "3000000000" +(format "%,%" 4294967296 4294967297) -- "4294967296,4294967297" + +;; as 'STR shares none of that code — pin it as the independent oracle. +(== (format "%" 1099511627776) (as 'STR 1099511627776)) -- true + +;; pivot builds column names from the key values +(set _t64 (table [g k v] (list ['a 'a 'b] [4294967296 4294967297 4294967296] [1.0 2.0 3.0]))) +(cols (pivot _t64 'g 'k 'v sum)) -- ['g '4294967296 '4294967297] From 7d370d481d72d67faf2a1f7ba405c737c9d43440 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Wed, 23 Sep 2026 16:17:25 +0300 Subject: [PATCH 12/13] fix(crash): format the banner with snprintf instead of macro string splicing The banner was spliced from string literals and the RAYFORCE_VERSION / RAYFORCE_GIT_COMMIT macros inside #ifdef arms; without the -D values a static analyser reads the literal followed by the bare macro name as two adjacent tokens and reports a syntax error. Format it with one snprintf at install time (the handler itself still never formats), with the macros defaulting to empty strings. Output unchanged. Co-Authored-By: Claude Fable 5.1 --- src/core/crash.c | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/core/crash.c b/src/core/crash.c index 76f894b2..41da2890 100644 --- a/src/core/crash.c +++ b/src/core/crash.c @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -71,21 +72,20 @@ static void cw_int(int v) { /* Banner precomputed at install time so the handler doesn't format it. */ static char g_banner[128]; -static void crash_banner_init(void) { - const char* v = -#ifdef RAYFORCE_VERSION - "rayforce " RAYFORCE_VERSION -#else - "rayforce" +#ifndef RAYFORCE_VERSION +#define RAYFORCE_VERSION "" #endif -#ifdef RAYFORCE_GIT_COMMIT - " (" RAYFORCE_GIT_COMMIT ")" +#ifndef RAYFORCE_GIT_COMMIT +#define RAYFORCE_GIT_COMMIT "" #endif - "\n"; - size_t vl = strlen(v); - if (vl >= sizeof(g_banner)) vl = sizeof(g_banner) - 1; - memcpy(g_banner, v, vl); - g_banner[vl] = '\0'; + +static void crash_banner_init(void) { + const char* ver = RAYFORCE_VERSION; + const char* rev = RAYFORCE_GIT_COMMIT; + int n = snprintf(g_banner, sizeof(g_banner), "rayforce%s%s%s%s%s\n", + ver[0] ? " " : "", ver, + rev[0] ? " (" : "", rev, rev[0] ? ")" : ""); + if (n < 0) g_banner[0] = '\0'; } #if defined(_WIN32) From 6b4de91312000ab4b322b5344b3efd9b28338510 Mon Sep 17 00:00:00 2001 From: Serhii Savchuk Date: Wed, 23 Sep 2026 16:28:19 +0300 Subject: [PATCH 13/13] test(regress): drop the 24 GB til from the i64 width probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `til` is eager, so counting a three-billion-element range built a 24 GB vector for no extra coverage — the neighbouring literals already cross 2^31, 2^32 and 2^63. Co-Authored-By: Claude Fable 5.1 --- test/rfl/regress/i64_text_width.rfl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/rfl/regress/i64_text_width.rfl b/test/rfl/regress/i64_text_width.rfl index 8ae3affc..cde583f7 100644 --- a/test/rfl/regress/i64_text_width.rfl +++ b/test/rfl/regress/i64_text_width.rfl @@ -7,7 +7,6 @@ (format "%" 1000000000000000000) -- "1000000000000000000" (format "%" 9223372036854775807) -- "9223372036854775807" (format "%" -9223372036854775806) -- "-9223372036854775806" -(format "%" (count (til 3000000000))) -- "3000000000" (format "%,%" 4294967296 4294967297) -- "4294967296,4294967297" ;; as 'STR shares none of that code — pin it as the independent oracle.