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" 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..e75b903ff 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,44 @@ 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 /* _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); +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..276f70f64 100644 --- a/packages/runtime/src/scr_win.c +++ b/packages/runtime/src/scr_win.c @@ -13,6 +13,116 @@ #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_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; + } +} + +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. */