Spec M38 -- mc hosted on Windows (arm64 and x64)

Owner's rule (2026-09-04): every landed OS/arch gets a release asset of mc itself. Goal: the self-hosted compiler runs natively on Windows on ARM64 and x64, bootstraps there to its own fixed point, mc build spawns lld-link and a taught compiler through kernel32, and mc-<VER>-windows-{arm64,x86_64} are release assets verified on the Windows runners. Depends on M19 (COFF arm64, lib/sys_windows.mc, scripts/sysroot-windows.sh, the windows-11-arm leg), on M20 (x86_64-win machine, coff-obj-x86_64, the x64 leg, lib/sys_windows_start.mc, scripts/test-windows.sh --arch x86_64) and on M37 (the host layer). Line references are to main at 77c4a62 (before M20).

What already exists #

Design #

1. The blocker: CreateProcessA has ten parameters #

src/arena.mc:51 -- #define MAXPARAMS 8 // never passes an argument on the stack -- is enforced at parse time (src/parse.mc:1935, at most 8 parameters) and again in src/gen_resolve.mc:510. CreateProcessA takes 10, so extern uptr CreateProcessA(...) does not compile today. This is the largest piece of M38 and the reason it is not a port of host_linux.mc.

Decision: caller- and callee-side stack parameters, MAXPARAMS 12 in src/. The x86-64 machine already passes its 7th and 8th arguments on the stack (x86_push_args, x86_param), and M20 does the same for the Win64 5th and later. The arm64 machine gains the same shape: arguments 9..12 stored at [sp, #0..#24] by the caller (the outgoing area is reserved with sub sp before the argument moves and released after the bl, 16-byte aligned, so back is 16 or 32), and read by the callee at [x29, #16 + 8*(i-8)] -- exactly where x86-64 reads its 7th at [rbp+16]. The ABI contract in docs/reference/objects.md § 4 changes from "parameters in x0..x7" to "parameters 1..8 in x0..x7, 9..12 on the stack", and scripts/check-surface.sh's assertions follow. MAXPARAMS is one #define in src/arena.mc, and the two checks that quote it change nothing else. The C seed keeps 8: stage0 only ever compiles src/mc.mc, which has no function with more than 8 parameters (scripts/check-limits.sh keeps proving every seed limit), so this is a documented divergence of the same kind as MAXSTRS/MAXGLOBALS/MAXOPEN (docs/build.md § limits). Tests: tests/mc/080-twelve-params.mc (a 12-parameter mc function summing its arguments, called directly and through callp, and a 10-parameter one whose arguments 9 and 10 are calls) runs on every target and every machine; the extern side is proved by CreateProcessA itself on the Windows runners.

Rejected: (B) #opcode/emit() trampolines per machine (M20 just deleted the last hand-written words from the Windows layer; two more would not generalise to the next Win32 entry point); (C) WinExec/ShellExecuteExA (no exit code / shell32).

2. src/host_windows.mc and the compiler runtime object #

host_windows.mc cannot define open/read/write/mmap: src/arena.mc:5-21 declares them extern, and func_add rejects a second definition (src/gen_resolve.mc:168, function declared twice). The Windows-hosted compiler resolves them the way every Windows test object already does -- from a runtime object linked next to it.

3. mmap: keep it, shim it #

src/arena.mc:32-46 keeps MAP_ANONP = 0x1022 portable across two kernels so that src/lexdump.mc, src/tomldump.mc, tools/bundle.mc and site/gen/main.mc -- which include arena.mc with no host file -- keep compiling. lib/sys_windows_host.mc provides an mmap with the identical prototype over VirtualAlloc(0, len, MEM_COMMIT|MEM_RESERVE (0x3000), PAGE_READWRITE (0x04)), ignoring addr, prot, flags, fd and off, returning 0 on failure. arena_map (src/arena.mc:199-208) already rounds to 64 KiB, VirtualAlloc's allocation granularity, and already treats both 0 and -1 as failure. arena.mc does not change.

4. Spawning #

src/driver.mc spawns in two places -- the linker (drv_link, :393) and the taught compiler (drv_teach, :468) -- through one function, drv_spawn (:151-162). Neither the driver's spawn path nor main.mc changes. lib/sys_windows_host.mc provides:

5. {sdk} and stdout capture #

The only child whose stdout mc captures is xcrun, through posix_spawn_file_actions_addopen (src/driver.mc:175-184). drv_sdk bails out before touching the file actions when host_has_sdk() is 0 (:171-172), so Windows needs no child stdout capture at all; the three symbols are stubs returning -1.

6. Paths and the .exe suffix #

7. The bootstrap chain #

scripts/bootstrap-windows.sh [SEED], bootstrap-linux.sh's sibling (no mc0: the C seed emits Mach-O only):

SEED src/mc_windows[_x86_64].mc -> build/mc1w.obj -> link -> build/mc1w.exe
build/mc1w.exe   same source     -> build/mc2w.obj -> link -> build/mc2w.exe
build/mc2w.exe   same source     -> build/mc3w.obj
cmp build/mc2w.obj build/mc3w.obj                       <- the criterion
sha256 build/mc2w.obj vs tests/golden/mc2-windows-<arch>.sha256
build/mc2w.exe --host  =>  os windows / arch <arch>     <- the seed check
scripts/test-windows.sh --arch <arch> --run-only build/tests-windows-<arch>
build/mc2w.exe --backend=macho src/mc.mc -o build/x-cross.o; cmp with build/mc2.o

scripts/link-windows.sh [--arch A] OUT IN.obj wraps lld-link -machine:<m> -subsystem:console -entry:mc_start -nodefaultlib -out:OUT IN.obj winstart.obj mcrt-windows-<arch>.obj kernel32.lib (dash-form options only: MSYS rewrites /out: into a path, PR #4). mcrt-windows-<arch>.obj, winstart.obj and kernel32.lib travel in the CI artifact as winrt.obj already does; with mc available locally the script rebuilds them. scripts/sysroot-windows.sh's .def grows CreateProcessA, WaitForSingleObject, GetExitCodeProcess, CreateDirectoryA, DeleteFileA, VirtualAlloc.

MSYS on the Windows runners: .exe names written explicitly everywhere ([ -x build/mc2w ] is not reliable); MSYS2_ARG_CONV_EXCL='*' in the job environment as the belt to the dash-form braces; CRLF: the repository has no .gitattributes and GitHub's Windows images ship core.autocrlf=true. M19/M20 survive because their runner only links and runs prebuilt objects; M38 makes it compile .mc sources and run shell scripts from the checkout, where #!/bin/sh\r is a hard failure. The lexer already treats \r as whitespace (src/lex.mc:655), so a CRLF checkout still produces the same tokens and the same object -- the golden is safe either way -- but the scripts are not: .gitattributes with * -text (Decision 11) and core.autocrlf=false set in the job before checkout.

8. CI and releases #

9. Makefile #

HOST := $(shell uname -s) reports MINGW64_NT-... or MSYS_NT-... under Git Bash, so the switch becomes a findstring; uname -m already reports aarch64/x86_64. REF = build/mc1w.exe, MC = build/mc2w.exe. Windows check = budget bootstrap-windows check-lex check-ast check-asm check-obj check-bundle check-mc check-toml check-limits check-skipped; everything else needs mc plus lld-link through scripts/link-windows.sh, and the four standalone tools link against the same mcrt object. check-skipped gains the Windows reasons.

Out of scope #

No .pdata/.xdata (M19's accepted gap, inherited). No mc sysroot fetch windows-* (M25). No wide-character paths (*A APIs, ANSI code page). No --exe for Windows (a PE writer): Windows, like Linux, always links through [linker]. No 32-bit Windows.

Files and estimated deltas #

filedelta
src/host_windows.mc; src/host_windows_{aarch64,x86_64}.mc; src/mc_windows{,_x86_64}.mcnew, ~60 + 7 + 7 + 15 + 15
lib/sys_windows_host.mcnew, ~200
src/machine_arm64.mc, src/machine_x86_64.mc, src/gen_walk.mc, src/arena.mc, src/parse.mc, src/gen_resolve.mc+60-120: stack parameters 9..12, MAXPARAMS 12
src/driver.mc+10 (host_exe_suffix at three sites); src/host_macos.mc, src/host_linux.mc +3 each
lib/sys_windows_start.mc+1 (third argument 0)
src/mc.windows-{aarch64,x86_64}{,-obj}.tomlnew, ~45 each
scripts/bootstrap-windows.sh, scripts/link-windows.shnew, ~220 + ~50
scripts/sysroot-windows.sh+8; scripts/check-*.sh +10 each (a Windows branch beside the Linux one)
scripts/check-surface.shthe ABI assertions for parameters 9..12
Makefile+70; .gitattributes new
tools/bundle.list + src/bundle_data.mc+4 entries, regenerated
.github/workflows/ci.yml / release.yml+180 / +140 net; scripts/release-assets.sh +20
tests/mc/080-twelve-params.mc; tests/golden/mc2-windows-{arm64,x86_64}.sha256new
docs/guide/95-windows-host.md (new, ~300), docs/reference/{hooks,objects,machine}.md, docs/bootstrap.md, docs/ci.md, docs/build.md, docs/core-language.md (parameter limit), docs/plan.md, CLAUDE.mdupdated

Acceptance #

  1. scripts/bootstrap-windows.sh reaches the fixed point on both windows-11-arm and windows-2025, and the golden matches.
  2. mc2w.exe --host says os windows / arch aarch64 (resp. x86_64) / sys sys_windows.
  3. Cross proof: mc2w.exe --backend=macho src/mc.mc -o x.o is byte-identical to the macOS job's build/mc2.o.
  4. scripts/test-windows.sh --run-only green on both architectures, run by a compiler that was itself bootstrapped on the runner.
  5. The Windows make check subset green; skipped targets each print a reason.
  6. mc build on Windows spawns lld-link and a taught compiler: a [compiler] project builds end to end with .exe suffixes (a tests/proj config in the Windows subset).
  7. Both release assets produced from a tag build, with .sha256, unpacked and run on the runner.
  8. macOS make check green; tests/mc/080-twelve-params.mc passes on every target incl. Linux and both Windows legs; check-surface's ABI assertions hold with the new stack-parameter rule; check-standalone still proves <mc/host> + <mc/core> + <user_default> == src/mc.mc. The three existing goldens move once (the parameter change touches the compiler); the two Windows goldens are recorded.

Risks #

Decisions (architect, 2026-09-04) #

  1. Stack parameters, not trampolines. MAXPARAMS 12 in src/ for definitions and calls, on all three machines; the seed keeps 8 as a documented divergence. M20 leaves MAXPARAMS at 8; this is M38's first step and its own commit.
  2. The POSIX shims live in a new bundled lib/sys_windows_host.mc, not in lib/sys_windows.mc.
  3. mmap shim over VirtualAlloc with the same prototype; arena.mc untouched.
  4. host_exe_suffix() joins the host interface (three call sites in the driver).
  5. host_environ() returns 0 on Windows; the child inherits.
  6. Paths stay /-only; the drive-letter gap is documented in the guide.
  7. GNU make on the Windows runners (choco install make), reusing the Makefile check subset.
  8. .tar.gz for the Windows assets too, mc.exe inside.
  9. Runners pinned: windows-11-arm and windows-2025 in both ci.yml and release.yml; the M20 x64 suite leg moves from windows-latest to windows-2025 in the same change.
  10. chmod is a no-op returning 0.
  11. .gitattributes with * -text.
  12. scripts/test-windows.sh --arch x86_64 is M20's.

Edit this page