SFEP-0021
Native Windows Self-Host (MSVC ABI)
- Status
- Accepted
- Type
- runtime
- Created
- Updated
- Author
- agent:compiler-architect
- Tracking
- #1485
Epic Design: Native Windows Self-Host (MSVC ABI)
Status: DESIGN GATE — not implemented. Decomposition / sequencing for review. Author: compiler architect Date: 2026-06-22
This is a design deliverable. No compiler code is written here. It enumerates the port surface, the ordered milestones, and the replace-vs-coexist call.
1. Goal
Make the Sailfin compiler self-host natively on a windows-latest runner,
producing a native MSVC-ABI binary (clang -target x86_64-pc-windows-msvc,
lld-link, MSVC CRT / UCRT), replacing the current cross-compile-from-Linux
mingw path (make ci-cross-windows). The end state: a windows-latest CI job
that runs <seed> build -p compiler natively, passes the suite, and verifies
the self-host fixed point — mirroring the Linux/macOS build+test+selfhost shape.
2. Current state (verified this session)
2.1 The runtime is already fully Sailfin — there is no C runtime to port
The premise in the task brief that C helpers under runtime/native/src/ must be
made MSVC-compatible is stale. runtime/native/ was deleted (#823); the
runtime capsule manifest declares c-sources = [] and ll-sources = []
(runtime/capsule.toml:34-35). Every runtime symbol is now a Sailfin module
under runtime/sfn/** plus runtime/prelude.sfn
(runtime/capsule.toml:49). The compiler emits LLVM IR for these and links them
into every binary.
Consequence: the “ABI port surface” (task item 1) is not a C-source port. It is (a) which target triple the emitted IR / clang compile uses, and (b) the handful of platform-divergent legs the compiler bakes at emit time. This collapses the single highest-risk unknown the brief worried about.
The only non-Sailfin artifact in the Windows link is
runtime/ir/windows_stubs.ll (hand-written LLVM IR, 83 lines), compiled only by
the mingw cross target and excluded from the runtime capsule
(runtime/ir/windows_stubs.ll:1-18).
2.2 The mingw cross path
make ci-cross-windows (Makefile:994-1144) consumes Linux-emitted
build/native/raw/*.ll, llvm-links them, compiles the linked bitcode with
clang -target x86_64-w64-mingw32 (Makefile:1040), re-emits a curated
RUNTIME_MODS list with SAILFIN_TARGET_OS=Windows for clock/exec/filesystem
(Makefile:1067,1087-1093), links windows_stubs.o + runtime objects + the
program with x86_64-w64-mingw32-gcc -static -lws2_32 (Makefile:1123-1128).
TLS is emulated via -femulated-tls (PR #1481, Makefile ~1040/1094). The
exe boots and runs the full frontend; --version and check exit 0.
The mingw path is IR-cross from Linux, not a self-host: the Windows exe never compiles anything on Windows. Self-host is what this epic adds.
2.3 The four native-build blockers (root causes traced)
| # | Symptom | Root cause (file:line) |
|---|---|---|
| 1 | enumerate_binary_capsule_sources hit iteration cap (4096) |
fs.listDirectory → sfn_fs_list_dir walks POSIX opendir/readdir and reads dirent.d_name at a hardcoded POSIX byte offset (runtime/sfn/adapters/filesystem.sfn:660-696, offsets 19/21 at :274-276). mingw’s readdir has a different struct dirent layout, so names come back garbage/empty; the BFS in capsule_resolver.sfn:3219-3261 re-enqueues bogus dirs and hits the cap at :3221. |
| 2 | The system cannot find the path specified. ×3 |
Subprocess/path. Two sub-causes: (a) emit-time host detection shells out (_shell_read_cmd("uname -s"), _get_env_cmd → printf), and run_capture is a stub on Windows returning -1 (runtime/sfn/platform/process_windows.sfn:182-184); (b) process.run (CreateProcessA) works but is invoked with POSIX path/flag assumptions for clang/linker. |
| 3 | cannot resolve self path ... binary_dir="." (#1482) |
exe_path() returns "" so binary_dir falls back to .. The Windows leg lowers GetModuleFileNameA (core_call_emission.sfn:805-820) but _resolve_self_path joins <dir>/sailfin with no .exe suffix (cli_main.sfn:1046-1052). Even with a correct path, the join misses the executable. |
| 4 | env-flag reads fail (#1483) | _get_env_cmd shells printf '%s' "$NAME" (cli_commands_utils.sfn:147) via _shell_read_cmd → run_capture (stubbed on Windows). _env_flag (cli_commands_utils.sfn:240) and the emit-time locators (lowering_debug_state.sfn:193,224,248,267) all route through it. Note env.get/getenv (runtime/sfn/io.sfn:97) works natively — only the shell-out path is broken. |
2.4 Toolchain reality
- Windows host: LLVM 22.1.8 (clang default target
x86_64-pc-windows-msvc,lld/lld-link), MSYS2, GnuWin32 make 3.81 (too old for this Makefile), WSL. - CI
windows-latest: ships MSVC Build Tools + Windows SDK (UCRT) and can install LLVM via choco/winget. No mingw needed for the MSVC path.
2.5 Seed / CI plumbing
.seed-version=0.7.0-alpha.46; one pin, read by Makefile + 4 workflows (Makefile:257,ci.yml:148-163).make fetch-seedrunsinstall.sh(Makefile:270-291) and downloads a Linux/macOS seed; there is no Windows seed asset today.release-tag.ymlbuilds per-OS via a matrix (release-tag.yml:43-55) and produces the Windows installer only as a mingw cross-build artifact off the linux leg (release-tag.yml:240-265). No native Windows release leg exists.ci.ymlhasbuild-compiler-linux(:139) andbuild-compiler-macos(:276) as the self-host build jobs; Windows is a cross-build step on the linux job (:183-246) plus the smoke job PR #1481 adds.
3. Constraints
- Self-host invariant. Every milestone must leave a compiler that still self-hosts on Linux/macOS. Windows-specific changes must be additive — emit-time legs selected by target, never replacing the POSIX leg.
- Three pillars untouched. Effects/capabilities/concurrency semantics do not change. This is a backend/runtime-platform epic.
- Memory cap is Linux-only.
apply_default_mem_limitis a no-op on Windows (windows_stubs.ll:28-30);compiler-safety.mdconfirms Darwin/Windows are no-ops. No change needed, but timeouts still apply. - No new bash e2e. Windows CI cannot rely on
bash; tests are*_test.sfn(no-bash-e2e.md). The emit-time host-detection rewrite (blocker 4) is what makes this possible — it removes the compiler’s own dependency onuname/sh. - Seed chicken-and-egg. A native Windows self-host needs a Windows seed that can already do native builds. Today’s only Windows binary is the mingw cross, which cannot native-build (blockers 1–4). Bootstrap must thread through it.
- TOML arrays single-line (
runtime/capsule.toml:27-33) — any manifest edit stays single-line.
4. Design
4.1 ABI port surface (MSVC) — concrete enumeration
Because the runtime is pure Sailfin IR, the MSVC port is a target-triple + emit-leg change, not a C-source port. Item by item:
| Surface | mingw today | MSVC target | Work |
|---|---|---|---|
| Compile target | clang -target x86_64-w64-mingw32 (Makefile:1040) |
clang -target x86_64-pc-windows-msvc (host default) |
Driver must pass the MSVC triple (or omit -target to use host default) when compiling emitted .ll → .o. |
| Linker | x86_64-w64-mingw32-gcc -static (Makefile:1123) |
clang driving lld-link (MSVC CRT) |
_clang_link_multi_with_opt (cli_main.sfn:1645-1745) appends -lm -lpthread (POSIX-only). On MSVC these must be dropped; -fuse-ld=lld and no -static (MSVC CRT is dynamic by default; /MT static-CRT optional). |
| CRT | mingw libc (-lm -lpthread -lws2_32) |
UCRT / MSVCRT | link-libs in runtime/capsule.toml:51 is ["-lm", "-lpthread"] — POSIX. Need a target-conditioned link-lib set (Windows: none required; ws2_32/kernel32 are auto-linked by clang-cl). |
| Exception model | setjmp/longjmp on a 256-byte heap jmp_buf (exception.sfn:136-138,199-213) |
Same — setjmp/longjmp are CRT functions on MSVC too |
The Sailfin exception runtime calls libc setjmp/longjmp and does not use SEH. MSVC UCRT provides setjmp/longjmp; the 256-byte buffer oversizes the MSVC jmp_buf (exception.sfn:52-57). No SEH interaction — this is the single biggest risk-reducer vs. the brief’s worry. Caveat: MSVC setjmp is a macro (_setjmp/_setjmpex); the emitted call @setjmp must bind to the CRT’s actual symbol — see Risk R1. |
| TLS | -femulated-tls (PR #1481) because native PE TLS faulted on the IR-cross image |
Re-test native MSVC TLS. The fault was specific to the mingw IR-cross image’s uninitialized PE TLS directory. A native clang-msvc compile emits a proper .tls section + _tls_index; native TLS likely works. Keep -femulated-tls as the safe default for milestone 1, drop it once native TLS is verified (own milestone). thread_local sfn_exception_frame_head_addr (exception.sfn:182) is the only TLS global. |
|
| Name mangling / calling conv | mingw uses SysV-ish + cdecl on x64 | MSVC x64 ABI | x86_64 Windows has one calling convention (Microsoft x64) for both ABIs, and Sailfin emits C-ABI i8*/i64/double signatures with no name mangling (bare @sfn_*). Emitted IR is ABI-neutral at the LLVM level; clang’s backend applies the MSVC x64 ABI when given the msvc triple. No source change — but verify struct-by-value ABI (the channel channel:<struct> work, #1472) lowers correctly under the MSVC struct-return rules (Risk R3). |
windows_stubs.ll |
provides apply_default_mem_limit, sailfin_runtime_serve, realpath→_fullpath, assert handlers (windows_stubs.ll:28-82) |
Same gaps exist under MSVC | Re-audit each stub under MSVC: _fullpath is UCRT (OK); realpath shim still needed; the no-op stubs are target-neutral IR (OK). The file stays Windows-only. Migrate it from a Makefile-only artifact into the driver’s Windows link path (see 4.2) so native self-host links it. |
| Emitted code C-ABI | bare i8*/i64/double, no mangling |
identical | Already ABI-neutral. The emit-time legs (errno symbol, exe_path, fs sentinels, clock id) are the only target-divergent pieces — all selected in lowering_debug_state.sfn and core_call_emission.sfn (:805-820 exe_path, :906-1088 fs sentinels). These already have a Windows leg; the work is making the selector not shell out (blocker 4). |
Net ABI work: target-triple + link-flag conditioning in the driver; verify TLS + setjmp symbol + struct-by-value ABI; no C porting.
4.2 Build-driver changes
The driver (cli_main.sfn + capsule_resolver.sfn) is the bulk of the epic.
Sub-items:
(A) Target-aware clang/linker invocation — _clang_link_multi_with_opt
(cli_main.sfn:1645-1745), _clang_compile_runtime_capsule_objects
(:894-1012), _emit_runtime_sfn_to_obj (:1145-1193):
- Introduce a single
_host_target_os()accessor (the non-shelling one from blocker 4) and a_target_triple()/_target_link_libs()/_target_no_static()helper set. On Windows: omit-lm/-lpthread(read fromruntime/capsule.tomllink-libsbut filter POSIX-only), omit-static, prefer-fuse-ld=lld(_resolve_linkeralready probes lld —cli_main.sfn:1692viacli_commands_utils.sfn_linker_on_path, which also shells out → must be de-shelled together with blocker 4). - Add
windows_stubs.llto the Windows runtime link inputs inassemble_runtime_capsule_link_inputs(cli_main.sfn:1485-1531) — currently only the Makefile cross target links it. This is what lets a native link resolverealpath/assert/serve stubs without the mingw Makefile. - Add
process_windows.sfnto the Windows runtime source set (it’s already in the mingwRUNTIME_MODSatMakefile:1067but not inruntime/capsule.toml:49). The native driver reads the capsule manifest, so the manifest needs a Windows-conditioned module set, OR the driver swapsprocess.sfn↔process_windows.sfnand excludesrlimit.sfnby target. (See R4 — this list-sync is currently guarded by a cross-windows e2e test.)
(B) fs-enumeration fix (blocker 1) — runtime/sfn/adapters/filesystem.sfn:
sfn_fs_list_dir(:666-696) must use the Win32FindFirstFileA/FindNextFileA/FindClosefamily on Windows instead ofopendir/readdir. This is the same module-split patternprocess_windows.sfnuses: either a per-target leg via the existingsailfin_intrinsic_fs_*sentinel seam (core_call_emission.sfn:906+) — add asailfin_intrinsic_fs_list_dirsentinel whose Windows leg lowersFindFirstFileA, or afilesystem_windows.sfnsibling in the Windows runtime set. Recommend the sentinel seam — it keeps onefilesystem.lland matches the existing fs intrinsic dispatch (mkdtemp/symlink/get_perms/existsalready do this). TheWIN32_FIND_DATA.cFileNameoffset replaces the dirent offset._fs_dirent_dname_offset(:660-664) probes/proc/self/statusto pick Linux vs Darwin — on Windows this is moot once the Win32 leg lands.
(C) subprocess/path-spawn fix (blocker 2) — two parts:
- The clang/linker spawn uses
process.run(CreateProcessA, real on Windows —process_windows.sfn:68-165). It works; the failure is path-shaped. Audit every constructed path/arg in the link/emit path for/vs\and for the.exeself-invocation (the child compiler is spawned as<self> emit ...—cli_main.sfn:1057,1064-1067). The child-spawn currently wraps insh -c "..."(cli_main.sfn:1064-1066) to clear env vars — thissh -cwrapper is itself a Windows blocker. Replace it withprocess.run_capture’s per-child env array (which the runtime supports) so no shell is needed. run_captureon Windows is a stub (process_windows.sfn:182-208). The child-compiler emit path and the_shell_read_cmdhost probes both need it. Implement realsfn_process_run_captureon Windows (CreatePipe + CreateProcessA with redirected stdout/stderr handles + ReadFile). This is the largest single runtime item; it un-stubs the whole capture family.
(D) self-path (#1482, blocker 3) — cli_main.sfn:1046-1052:
_resolve_self_pathmust try<binary_dir>/sailfin.exe(andsfn.exe) on Windows. Cleanest: havefn mainthread the already-resolvedexe_path()straight into_resolve_self_pathrather than re-deriving<dir>/sailfin—exe_path()returns the real running-binary path including.exe. The WindowsGetModuleFileNameAleg (core_call_emission.sfn:805-820) already returns the full path; the bug is the driver discarding it and reconstructing a suffixless name. Fix: store the full exe path in a module global atfn mainentry and use it as the self-invocation argv[0] for child emits.
(E) env-flag de-shell (#1483, blocker 4) — cli_commands_utils.sfn:147 and
all locators in lowering_debug_state.sfn:
- Replace
_get_env_cmd(printf via_shell_read_cmd) with a directenv.get(name)(runtime/sfn/io.sfnsfn_getenv, which works on Windows). This is a strict improvement on all platforms (no fork per env read) and is the keystone fix — it removes the compiler’s dependency onsh/printffor flags. - Replace the
_shell_read_cmd("uname -s")host probe in the four locators (lowering_debug_state.sfn:194,224,249,268) with a non-shelling host-OS detector. Options: (a) asailfin_intrinsic_host_ossentinel the compiler resolves at its own build time (the host is known when the compiler is compiled); (b) read a compile-baked constant. Recommend ahost_os()runtime helper that uses a platform primitive (GetVersion/file probe) so a single binary stays correct, mirroring how_fs_dirent_dname_offsetprobes/proc. TheSAILFIN_TARGET_OSoverride (read viaenv.getnow) stays for cross-compile.
4.3 Bootstrapping / seed
The chicken-and-egg: native self-host needs a Windows seed that can native-build, but the only Windows binary today (mingw cross) cannot (blockers 1–4).
Bootstrap sequence:
- Stage 0 (today): mingw-cross exe boots + runs the frontend (PR #1481). It cannot native-build.
- Stage 1 — make the cross exe native-build-capable. Land blockers 1–4
(fs-enum, run_capture, self-path, env de-shell) in
compiler/src+runtime/sfn. Because these are compiler/runtime source changes, the Linux self-host builds the new compiler from the old seed, and the mingw-cross of that new compiler is now a Windows exe that can native-build. No Windows seed needed yet — the cross exe is the bootstrap seed. - Stage 2 — native MSVC build from the cross seed. On
windows-latest, take the Stage-1 mingw-cross exe as the seed, run<seed> build -p compilerwith the MSVC target. Output: the first native MSVC compiler (pass-1). - Stage 3 — native MSVC self-host. Pass-1 builds pass-2 (seedcheck). Verify pass-2 == fixed point and runs hello-world. This is the self-host proof.
- Stage 4 — publish a native Windows seed. Once Stage 3 is green in CI,
release-tag.ymlpublishes a native MSVCsailfin-windows-x86_64asset./pin-seedbumps.seed-version;make fetch-seed/install.shlearn to download the Windows asset (EXE_EXT=.exe, the Makefile already has the var atMakefile:268). From here, native Windows self-host uses a native seed and the mingw-cross seed is retired.
Key insight: the mingw-cross exe is the bootstrap vehicle, not a permanent dependency. It only needs to live long enough to build the first native exe (Stage 2). The cross path retires after Stage 5 (see replace-vs-coexist, §6).
.seed-version / fetch-seed / release-tag evolution:
install.sh+make fetch-seed: add a Windows asset name +.exehandling. TheFETCHED_SEEDvar already accounts for$(EXE_EXT)(Makefile:268).release-tag.yml: add a nativewindows-x86_64matrix leg (runs-on: windows-latest) that self-host-builds + packages the.exeinstaller, replacing the linux-leg mingw cross artifact (:240-265).- Seed-blocker discipline: blockers 1–4 + the run_capture port carry the
seed-blockerlabel — they must be in the pinned seed before a native Windows seed is cut.
4.4 CI architecture
Mirror the per-OS build-compiler job shape:
- New
build-compiler-windowsjob (ci.yml, sibling ofbuild-compiler-macosat:276):runs-on: windows-latest, install LLVM (clang+lld) via choco, fetch the seed (Stage 1 cross exe initially; native exe after Stage 4), run the sailfin-build action withtarget: windows-x86_64, upload the build tree. - New
build-test-windowsshard job (sibling ofbuild-test-linuxat:368): consume the Windows build tree, run the suite, run the triple-pass self-host verify (theselfhost1/selfhost2shape atci.yml:623-632). - Makefile: the GnuWin32 make 3.81 on the host is too old; CI should drive
the build through the sailfin-build composite action (already used by
Linux/macOS —
ci.yml:172) or a thin PowerShell script, not the Makefile’s bash-heavy targets. Theci-cross-windowstarget stays for the bootstrap window only.
4.5 Recommended replace-vs-coexist call
Coexist during the transition, then replace. Concretely:
- Keep
ci-cross-windows+ the smoke job through Stages 1–3 — it is the only thing producing a Windows binary and is the bootstrap seed source. - Once
build-test-windows(native MSVC self-host) is green for one full release cycle and a native Windows seed is published (Stage 4–5), retireci-cross-windows, theRUNTIME_MODSMakefile loop, and therelease-tag.ymllinux-leg Windows artifact. Keepwindows_stubs.llandprocess_windows.sfn(now consumed by the native driver). Delete-femulated-tlsonly after native TLS is verified (its own milestone).
Rationale: replacing before the native path proves out would leave Windows with no working binary if the MSVC path hits an unknown. The cross path is cheap to keep (one Makefile target) and is the safety net.
5. Phasing & sequencing (milestone table)
Sizes are XS/S/M (never L per issue contract). “Blocked by” gives the DAG.
| # | Milestone | Scope | Size | Blocked by |
|---|---|---|---|---|
| M0 | env-flag de-shell (#1483) | Replace _get_env_cmd printf shell-out with env.get; de-shell _linker_on_path. Keystone — removes sh/printf dep on all platforms. |
S | — |
| M1 | host-OS detection de-shell | Replace _shell_read_cmd("uname -s") in the 4 locators (lowering_debug_state.sfn) with a non-shelling host_os() (sentinel or runtime probe). |
S | M0 |
| M2 | self-path .exe (#1482) |
Thread exe_path() full path into _resolve_self_path; handle .exe. |
XS | M0 |
| M3 | Windows run_capture |
Real sfn_process_run_capture (CreatePipe + CreateProcessA + ReadFile) in process_windows.sfn; un-stub the capture family. |
M | — |
| M4 | child-spawn de-shell | Replace the sh -c "..." env-clearing child-emit wrapper (cli_main.sfn:1064-1066) with run_capture per-child env. |
S | M0, M3 |
| M5 | fs enumeration (blocker 1) | sailfin_intrinsic_fs_list_dir sentinel + Win32 FindFirstFile leg; fix the dirent-offset enumeration on Windows. |
M | M1 |
| M6 | driver target conditioning | _target_triple/_target_link_libs/no-static/lld for MSVC; add windows_stubs.ll + process_windows.sfn to the native Windows link/runtime set. |
M | M0, M1 |
| M7 | first native MSVC build (booting) | On windows-latest, cross-seed → <seed> build -p compiler MSVC → a booting native exe (--version, check). Smallest first natively-built booting binary. |
S | M2, M4, M5, M6 |
| M8 | native self-host fixed point | pass-1 → pass-2 == fixed point; hello-world runs. | S | M7 |
| M9 | CI build-compiler-windows + build-test-windows |
Mirror macOS jobs; suite + triple-pass selfhost verify. | M | M8 |
| M10 | native TLS (drop -femulated-tls) |
Verify native MSVC PE TLS for the one thread_local; drop the flag. |
S | M7 |
| M11 | native Windows seed + release | release-tag.yml native leg, install.sh/fetch-seed .exe, /pin-seed. |
M | M9 |
| M12 | retire mingw cross | Delete ci-cross-windows, RUNTIME_MODS loop, linux-leg Windows artifact. |
S | M11 (+1 cycle) |
Smallest first deliverable that yields a natively-built, booting Windows binary: M7, gated by M2+M4+M5+M6 (and transitively M0/M1/M3). M0 is the true unblocker — almost everything depends on de-shelling.
Mapping the brief’s issues: #1482 → M2; #1483 → M0. New issues: M1 (host-OS), M3 (run_capture), M4 (child-spawn), M5 (fs-enum), M6 (driver target), M7–M12 (build/toolchains/seed/CI).
Bundling note (decomposition discipline): M5’s fs-enum capability (the
fs_list_dir sentinel) and its sole consumer (the Windows leg) land in one
PR — splitting would force a seed cut between the sentinel and its use. Same
for M3’s run_capture runtime body + M4’s consumer — but M4 also depends on M0,
so they are honestly separable. M0+M1+M2 are independent small wins that could
ship as one “de-shell the compiler” PR if grooming prefers, but M0 alone is the
load-bearing one and is worth landing first to de-risk everything else.
6. Risks & invariants
Invariants preserved: self-host (every milestone keeps Linux/macOS green —
all changes are additive emit-legs/target-conditions); three pillars untouched
(backend epic); memory cap (Windows no-op, unchanged); no bash e2e (M0/M1 remove
the compiler’s own sh dependency, enabling this).
What could regress Linux/macOS: M0/M1 (env + host-OS) touch hot,
all-platform code paths. env.get must match the printf-capture semantics
exactly (empty vs unset, trailing-newline trim — _shell_read_cmd trims via
_toml_trim_cmd, cli_commands_utils.sfn:144). M6’s link-flag filtering must
not drop -lm/-lpthread on POSIX. Both gated by full make check on
Linux+macOS before merge.
Top 3 risks + mitigations:
-
R1 —
setjmp/longjmpsymbol binding under MSVC UCRT. MSVC’ssetjmpis a macro expanding to_setjmp/_setjmpexwith a second frame-pointer argument; the runtime emits a barecall i32 @setjmp(i8*)(exception.sfn:136). If clang-msvc does not redirect@setjmpto the 2-arg CRT entry, the exception runtime corrupts on firsttry. Mitigation: verify in M7 with a minimal try/throw fixture; if it mis-binds, add a Windows-leg sentinel (like errno) that emits the correct_setjmpexcall, or provide awindows_stubs.llsetjmpshim. Detection: the existing exception e2e (test_runtime_exception_frameslineage) on the native exe. -
R2 —
run_capture(M3) is the deepest unknown. The whole self-host build spawns the child compiler and captures output; the Win32 pipe + redirected- handle dance (CreatePipe, SetHandleInformation, overlapped vs blocking reads, deadlock on full pipe buffers) is subtle and currently entirely stubbed. Mitigation: land M3 standalone with a dedicated*_test.sfnthat spawns a known child and asserts captured stdout before it’s on the self-host critical path; model on the POSIX body’s pipe/poll structure (process.sfn:499+). Use blocking reads with both pipes drained on separate reads to avoid the classic full-buffer deadlock. -
R3 — struct-by-value ABI divergence (Microsoft x64). Recent concurrency work added by-value struct channel elements (#1472) and struct returns. The Microsoft x64 ABI passes/returns aggregates differently from SysV (>8/16-byte structs go by hidden pointer; specific size rules differ). If the emitter’s IR assumes SysV aggregate lowering anywhere, MSVC codegen silently mismatches. Mitigation: the emitter produces typed LLVM aggregates and lets clang’s backend apply the target ABI, so this should be transparent — but verify with a struct-channel + struct-return fixture in M7/M8. Detection: a value round-trip test; failure mode is garbage struct fields, caught by an equality assert.
Honorable mention (downgraded by §2.1): the brief’s headline risk — C
runtime MSVC incompatibility — is largely retired: there is no C runtime.
The residual is windows_stubs.ll (4 small IR stubs, target-neutral) and the
emit-time legs, both already partially Windows-aware.
7. Verification
Per-milestone (run on the relevant OS):
- M0/M1/M2:
sfn check compiler/src/, thenmake checkon Linux and macOS — these touch all-platform code; the gate is no Linux/macOS regression. - M3: new
process_run_capture_windows_test.sfnonwindows-latestspawns a child that prints a sentinel and asserts captured stdout == sentinel. - M4: child-emit path produces byte-identical
.oon Linux (no behavior change) + works on Windows. - M5:
enumerate_binary_capsule_sourcesovercompiler/srcreturns the full module set on Windows (no iteration-cap warning); assert count == Linux count. - M6: native
clang -target x86_64-pc-windows-msvccompile of one emitted.lllinks withlld-link; no-lm/-lpthread/-static. - M7:
<cross-seed> build -p compileronwindows-latest→ exe; exe--versionandcheck examples/basics/hello-world.sfnexit 0; try/throw + struct-channel fixtures pass (R1/R3 gate). - M8: triple-pass — pass1 builds pass2,
fc /b(binary compare) or hash-equal fixed point;sailfin run examples/basics/hello-world.sfnexit 0. - M9: CI
build-test-windowsgreen (full suite + selfhost verify). - M10: M7 fixtures pass with
-femulated-tlsremoved. - M11:
make fetch-seeddownloads the Windows.exe;<native-seed> build -p compilerself-hosts. - Final: Linux + macOS + Windows all run
make check-equivalent self-host fixed point green; mingw cross retired (M12).
8. Future considerations (1.0 interactions)
- Concurrency runtime (pillar 3). The structured-concurrency work
(
runtime/sfn/concurrency/*, recent #1472/#1474/#1477) uses POSIX threads (-lpthread). Windows needs a Win32-threads or C11-threads leg; theprocess_windows.sfnsplit pattern is the template. This epic does not block on it (the scheduler/nursery modules are incapsule.toml:49but the Windows runtime set can stub them initially via thewindows_stubs.llserveno-op precedent), but full Windows concurrency is a follow-on epic. Flag it so M6’s runtime-set decision leaves room. runtime/capsule.tomlper-target source sets. This epic forces a target-conditioned runtime module list (process vs process_windows, rlimit exclusion, stubs). That mechanism (a[target.windows]overlay or driver-side swap) is reusable for the concurrency-leg work and for any futuresfn build --targetcross-compile — design M6’s mechanism to generalize, not to special-case Windows inline.sfn build --target=x86_64-pc-windows-msvc. The Makefile comments repeatedly promise that a real--targetflag retires the cross hack (Makefile:988,windows_stubs.ll:18). M6’s target-conditioning is exactly the foundation for that flag; landing it as a first-class--target(not a Windows special-case) pays forward to Linux→Windows and macOS→Windows cross-compile without re-architecting.- Multi-line TOML arrays. The single-line constraint
(
runtime/capsule.toml:27-33) will fight a[target.*]overlay; the parser extension is a small predecessor if M6 chooses the manifest-overlay route over the driver-swap route.