From af144dfff42fdcdba23143889ca2b34dc4409109 Mon Sep 17 00:00:00 2001 From: swadhinbiswas Date: Tue, 28 Jul 2026 14:18:39 +0600 Subject: [PATCH 1/6] runtime: add MSVC POSIX shims for Windows native builds The runtime assumed mingw-w64 on Windows, which provides POSIX headers and functions (ssize_t, clock_gettime, nanosleep, dirent.h, unistd.h) that MSVC's CRT does not ship. Users opening VS2022 Developer Command Prompt get MSVC's bundled clang (i686-pc-windows-msvc) instead of mingw-w64, and every compilation fails with missing type/function errors. Add _MSC_VER-guarded shims in scr_win.c: - clock_gettime() over QueryPerformanceCounter (monotonic) and GetSystemTimeAsFileTime (realtime) - nanosleep() over Sleep() - opendir/readdir/closedir over FindFirstFileW/FindNextFileW - CLOCK_REALTIME, CLOCK_MONOTONIC, struct timespec declarations Guard POSIX header includes in scr_lib.c, scr_path.c, scr_url.c with _MSC_VER checks, providing CRT equivalents (_getcwd, _access, _isatty) where needed. Fixes #25 --- packages/runtime/src/scr_lib.c | 18 ++++- packages/runtime/src/scr_path.c | 5 ++ packages/runtime/src/scr_runtime.h | 13 ++++ packages/runtime/src/scr_url.c | 2 + packages/runtime/src/scr_win.c | 109 +++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 0d965973d..ce40e4ef4 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -17,7 +17,17 @@ #include "scr_runtime.h" #include -#include +#ifndef _MSC_VER +#include /* mingw-w64 ships one; MSVC: see scr_win.c shims */ +#else +/* MSVC dirent shim — declared here, defined in scr_win.c. */ +enum { DT_REG = 8, DT_DIR = 4 }; +struct dirent { char d_name[260]; unsigned char d_type; }; +typedef struct { void *_hFind; int _first; struct dirent _ent; } DIR; +DIR *opendir(const char *path); +struct dirent *readdir(DIR *d); +int closedir(DIR *d); +#endif #include #include #include @@ -44,7 +54,13 @@ #include /* _mkdir */ #include /* _isatty, _access, open/read/write/close */ #include /* getpid */ +#ifndef _MSC_VER #include /* mingw-w64 ships one: getcwd, access, isatty, ... */ +#else +#define getcwd _getcwd +#define access _access +#define isatty _isatty +#endif #include /* BEFORE windows.h (which pulls winsock 1 otherwise) */ #include /* inet_ntop, sockaddr_in6 */ #include /* GetAdaptersAddresses (os.networkInterfaces) */ diff --git a/packages/runtime/src/scr_path.c b/packages/runtime/src/scr_path.c index 0a21f3eb3..d9f11e209 100644 --- a/packages/runtime/src/scr_path.c +++ b/packages/runtime/src/scr_path.c @@ -22,7 +22,12 @@ #include #include #include +#ifndef _MSC_VER #include +#else +#include /* _getcwd */ +#define getcwd _getcwd +#endif /* ── a tiny growable byte buffer ─────────────────────────────────────── */ diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 0d359b660..c0273c837 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -13,7 +13,12 @@ #include #include #include /* memcpy in the inline slot accessors */ +#ifdef _MSC_VER +#include /* SSIZE_T on MSVC */ +typedef SSIZE_T ssize_t; +#else #include /* ssize_t in the transport ops table */ +#endif /* ── win32 libc shims (scr_win.c; see the windows portability inventory) ── * The POSIX/BSD functions the runtime calls that mingw-w64's CRT does not @@ -31,6 +36,14 @@ char *stpcpy(char *dst, const char *src); void arc4random_buf(void *buf, size_t n); struct tm *gmtime_r(const time_t *t, struct tm *out); char *strcasestr(const char *hay, const char *needle); +#ifdef _MSC_VER +#include +#define CLOCK_REALTIME 0 +#define CLOCK_MONOTONIC 1 +struct timespec { time_t tv_sec; long tv_nsec; }; +int clock_gettime(int clk_id, struct timespec *ts); +int nanosleep(const struct timespec *req, struct timespec *rem); +#endif /* _MSC_VER */ #endif /* ── process ──────────────────────────────────────────────────────────── */ diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c index 777440518..e8496f8ea 100644 --- a/packages/runtime/src/scr_url.c +++ b/packages/runtime/src/scr_url.c @@ -32,7 +32,9 @@ #include #include #include +#ifndef _MSC_VER #include +#endif ScrUrl *scr_url_retain(ScrUrl *u) { if (u->rc != SIZE_MAX) u->rc++; diff --git a/packages/runtime/src/scr_win.c b/packages/runtime/src/scr_win.c index 3ce9535bf..2bb2d4dc7 100644 --- a/packages/runtime/src/scr_win.c +++ b/packages/runtime/src/scr_win.c @@ -13,6 +13,115 @@ #include #include +/* ── MSVC POSIX shims ──────────────────────────────────────────────── + * mingw-w64 provides POSIX headers/functions (unistd.h, dirent.h, + * clock_gettime, nanosleep). MSVC's CRT does not — these shims + * bridge the gap so the runtime compiles under both toolchains. */ +#ifdef _MSC_VER + +#ifndef CLOCK_REALTIME +#define CLOCK_REALTIME 0 +#endif +#ifndef CLOCK_MONOTONIC +#define CLOCK_MONOTONIC 1 +#endif + +int clock_gettime(int clk_id, struct timespec *ts) { + (void)clk_id; + /* QueryPerformanceCounter is the only high-res monotonic clock on + * Windows; its epoch is arbitrary but monotonic — sufficient for + * elapsed-time measurements. For CLOCK_REALTIME we use + * GetSystemTimeAsFileTime which is UTC since 1601. */ + if (clk_id == CLOCK_REALTIME) { + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + ULARGE_INTEGER li; + li.LowPart = ft.dwLowDateTime; + li.HighPart = ft.dwHighDateTime; + /* FILETIME is 100-ns intervals since 1601-01-01. + * Unix epoch offset: 11644473600 seconds = 116444736000000000 * 100ns. */ + li.QuadPart -= 116444736000000000ULL; + ts->tv_sec = (time_t)(li.QuadPart / 10000000ULL); + ts->tv_nsec = (long)((li.QuadPart % 10000000ULL) * 100); + return 0; + } + /* CLOCK_MONOTONIC — QueryPerformanceCounter. */ + static LARGE_INTEGER freq = {0}; + if (freq.QuadPart == 0) QueryPerformanceFrequency(&freq); + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + ts->tv_sec = (time_t)(now.QuadPart / freq.QuadPart); + ts->tv_nsec = (long)((now.QuadPart % freq.QuadPart) * 1000000000LL / freq.QuadPart); + return 0; +} + +int nanosleep(const struct timespec *req, struct timespec *rem) { + if (rem) { rem->tv_sec = 0; rem->tv_nsec = 0; } + /* Sleep takes milliseconds; ceil to avoid sleeping too short. */ + DWORD ms = (DWORD)(req->tv_sec * 1000 + (req->tv_nsec + 999999) / 1000000); + if (ms == 0) ms = 1; /* Sleep(0) yields the timeslice */ + Sleep(ms); + return 0; +} + +/* Minimal shim for MSVC — provides opendir/readdir/closedir + * and the d_type constants over FindFirstFileW/FindNextFileW. Enough + * for scr_lib.c's readdir loops; not a full POSIX emulation. */ +#include + +struct dirent { + char d_name[260]; + unsigned char d_type; +}; + +enum { DT_REG = 8, DT_DIR = 4 }; + +typedef struct { + HANDLE hFind; + WIN32_FIND_DATAW fdata; + struct dirent entry; + int first; +} DIR; + +DIR *opendir(const char *path) { + DIR *d = (DIR *)malloc(sizeof *d); + if (!d) return NULL; + /* Build wildcard path: "path\*" */ + wchar_t wpath[MAX_PATH * 2]; + MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH); + wcscat(wpath, L"\\*"); + d->hFind = FindFirstFileW(wpath, &d->fdata); + d->first = 1; + if (d->hFind == INVALID_HANDLE_VALUE) { free(d); return NULL; } + return d; +} + +struct dirent *readdir(DIR *d) { + for (;;) { + if (d->first) { d->first = 0; } + else if (!FindNextFileW(d->hFind, &d->fdata)) { return NULL; } + /* Skip . and .. */ + if (d->fdata.cFileName[0] == L'.' && + (d->fdata.cFileName[1] == L'\0' || + (d->fdata.cFileName[1] == L'.' && d->fdata.cFileName[2] == L'\0'))) + continue; + WideCharToMultiByte(CP_UTF8, 0, d->fdata.cFileName, -1, + d->entry.d_name, sizeof d->entry.d_name, NULL, NULL); + d->entry.d_type = (d->fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + ? DT_DIR : DT_REG; + return &d->entry; + } +} + +int closedir(DIR *d) { + if (!d) return -1; + if (d->hFind != INVALID_HANDLE_VALUE) FindClose(d->hFind); + free(d); + return 0; +} + +#endif /* _MSC_VER */ + /* POSIX.1-2008 stpcpy: strcpy returning the END of the copy — scr_number.c * (untouchable by project rule; ryu-adjacent) builds "e+"/"e-" exponent * tails with it. */ From 55cbb4d1e30d602ba8435e1d748eb5e121b962ca Mon Sep 17 00:00:00 2001 From: swadhinbiswas Date: Tue, 28 Jul 2026 14:27:01 +0600 Subject: [PATCH 2/6] ci: add MSVC verification workflow for #25 Run on push/PR to fix/msvc-posix-shims only. Tests the exact scenario from #25: compiling the runtime with MSVC's bundled clang (no mingw, no zigcc) on windows-latest, including the Map + sort pattern that bare.ts uses. Also runs a Linux corpus smoke test to verify no regressions. --- .github/workflows/msvc-verify.yml | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/msvc-verify.yml diff --git a/.github/workflows/msvc-verify.yml b/.github/workflows/msvc-verify.yml new file mode 100644 index 000000000..cce8b5430 --- /dev/null +++ b/.github/workflows/msvc-verify.yml @@ -0,0 +1,66 @@ +name: MSVC POSIX Shims + +on: + push: + branches: [fix/msvc-posix-shims] + pull_request: + branches: [fix/msvc-posix-shims] + +jobs: + msvc_compile: + name: verify MSVC compilation + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - name: Compile runtime with MSVC clang + shell: pwsh + run: | + # Verify clang is MSVC-targeted (not mingw) + clang --version + # Compile a simple program — this is the exact scenario from #25 + node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend c + - name: Compile Map + sort program (the bare.ts pattern) + shell: pwsh + run: | + $ts = @' + const m = new Map(); + m.set(1, 10); + m.set(2, 5); + m.set(3, 20); + const sorted = [...m.entries()].sort((a, b) => a[1] - b[1]); + console.log(sorted[0][0], sorted[0][1]); + '@ + $ts | Out-File -Encoding utf8 test-msvc.ts + node packages/cli/dist/main.js run test-msvc.ts --backend c + + linux_regression: + name: verify no Linux regressions + runs-on: ubuntu-24.04 + timeout-minutes: 10 + env: + SCRIPTC_NO_CACHE: "1" + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - name: Corpus smoke tests + run: >- + SCRIPTC_TEST_WORKERS=4 pnpm exec vitest run tests/harness/differential.test.ts + -t "001-hello|518-array-sort|520-map-basics|path" From 9bebebb44293150e2ac4cd2535d08d8a6bf905f7 Mon Sep 17 00:00:00 2001 From: swadhinbiswas Date: Tue, 28 Jul 2026 14:31:40 +0600 Subject: [PATCH 3/6] runtime: guard struct timespec and drop windows.h from header - Remove #include from scr_runtime.h's _MSC_VER block: it pulled winsock.h (via windows.h) into every TU, conflicting with winsock2.h included later in scr_lib.c - Guard struct timespec with #ifndef _TIMESPEC_DEFINED: modern UCRT already defines it, so redefinition caused C1104 errors on CI --- packages/runtime/src/scr_runtime.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index c0273c837..6af543296 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -37,10 +37,12 @@ void arc4random_buf(void *buf, size_t n); struct tm *gmtime_r(const time_t *t, struct tm *out); char *strcasestr(const char *hay, const char *needle); #ifdef _MSC_VER -#include +#ifndef _TIMESPEC_DEFINED +#define _TIMESPEC_DEFINED +struct timespec { time_t tv_sec; long tv_nsec; }; +#endif #define CLOCK_REALTIME 0 #define CLOCK_MONOTONIC 1 -struct timespec { time_t tv_sec; long tv_nsec; }; int clock_gettime(int clk_id, struct timespec *ts); int nanosleep(const struct timespec *req, struct timespec *rem); #endif /* _MSC_VER */ From 042c37dbbd31d3a9da9291a879bdeccdca539105 Mon Sep 17 00:00:00 2001 From: swadhinbiswas Date: Tue, 28 Jul 2026 14:36:07 +0600 Subject: [PATCH 4/6] =?UTF-8?q?runtime:=20remove=20redundant=20struct=20ti?= =?UTF-8?q?mespec=20=E2=80=94=20UCRT=20already=20defines=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The _TIMESPEC_DEFINED guard didn't work because UCRT's time.h defines struct timespec (line 45) but doesn't set _TIMESPEC_DEFINED in the clang/MSVC mode we're compiling in. Remove the definition entirely — UCRT 10.0.26100.0 provides it, and scr_win.c's clock_gettime/nanosleep use it without redefining. --- packages/runtime/src/scr_runtime.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 6af543296..93969ed20 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -37,10 +37,6 @@ void arc4random_buf(void *buf, size_t n); struct tm *gmtime_r(const time_t *t, struct tm *out); char *strcasestr(const char *hay, const char *needle); #ifdef _MSC_VER -#ifndef _TIMESPEC_DEFINED -#define _TIMESPEC_DEFINED -struct timespec { time_t tv_sec; long tv_nsec; }; -#endif #define CLOCK_REALTIME 0 #define CLOCK_MONOTONIC 1 int clock_gettime(int clk_id, struct timespec *ts); From 80d27b78c00563945cd1ed30b8bb1ec695c648a6 Mon Sep 17 00:00:00 2001 From: swadhinbiswas Date: Tue, 28 Jul 2026 14:47:02 +0600 Subject: [PATCH 5/6] runtime: add POSIX compat shims for MSVC (PATH_MAX, mode_t, S_IS*) MSVC CRT lacks several POSIX constants/types used throughout the runtime: - PATH_MAX (use _MAX_PATH from stdlib.h) - mode_t (typedef unsigned int) - F_OK (value 0) - S_ISDIR/S_ISREG macros (use _S_IFMT/_S_IFDIR/_S_IFREG from sys/stat.h) - S_ISLNK/S_ISFIFO/S_ISSOCK/S_ISBLK/S_ISCHR (stub as 0 on Windows) These are guarded by _MSC_VER so mingw-w64 and Linux are unaffected. --- packages/runtime/src/scr_runtime.h | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 93969ed20..e75b903ff 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -37,6 +37,38 @@ void arc4random_buf(void *buf, size_t n); struct tm *gmtime_r(const time_t *t, struct tm *out); char *strcasestr(const char *hay, const char *needle); #ifdef _MSC_VER +#include /* _MAX_PATH */ +#include /* _S_IFMT, _S_IFDIR, _S_IFREG */ +#ifndef PATH_MAX +#define PATH_MAX _MAX_PATH +#endif +#ifndef F_OK +#define F_OK 0 +#endif +#ifndef S_ISDIR +#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) +#endif +#ifndef S_ISREG +#define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG) +#endif +#ifndef S_ISLNK +#define S_ISLNK(m) (0) +#endif +#ifndef S_ISFIFO +#define S_ISFIFO(m) (0) +#endif +#ifndef S_ISSOCK +#define S_ISSOCK(m) (0) +#endif +#ifndef S_ISBLK +#define S_ISBLK(m) (0) +#endif +#ifndef S_ISCHR +#define S_ISCHR(m) (0) +#endif +#ifndef _mode_t_defined +typedef unsigned int mode_t; +#endif #define CLOCK_REALTIME 0 #define CLOCK_MONOTONIC 1 int clock_gettime(int clk_id, struct timespec *ts); From 02c5a90604fed218924c2f60049e10eb8b9280ea Mon Sep 17 00:00:00 2001 From: swadhinbiswas Date: Tue, 28 Jul 2026 16:32:30 +0600 Subject: [PATCH 6/6] runtime: null-terminate d_name in MSVC readdir shim WideCharToMultiByte silently drops the null terminator when the UTF-8 output fills all 260 bytes of d_name. Force-terminate after the conversion to prevent out-of-bounds reads by callers. --- packages/runtime/src/scr_win.c | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime/src/scr_win.c b/packages/runtime/src/scr_win.c index 2bb2d4dc7..276f70f64 100644 --- a/packages/runtime/src/scr_win.c +++ b/packages/runtime/src/scr_win.c @@ -107,6 +107,7 @@ struct dirent *readdir(DIR *d) { continue; WideCharToMultiByte(CP_UTF8, 0, d->fdata.cFileName, -1, d->entry.d_name, sizeof d->entry.d_name, NULL, NULL); + d->entry.d_name[sizeof d->entry.d_name - 1] = '\0'; d->entry.d_type = (d->fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? DT_DIR : DT_REG; return &d->entry;