feat(windows): a Windows target for ludicc, and the compiler builds itself there

--target <triple> (default: the host, from OS=Windows_NT) selects the Windows
runtime. emit_win.ludic defines the POSIX names the backend already calls over
the UCRT and Win32 in IR, so rename replaces an existing file, ftell is 64-bit,
and fopen is binary. Known folders follow %APPDATA% / %LOCALAPPDATA% / %TEMP%,
and the driver speaks cmd.exe, writes .exe outputs and finds LLVM's clang.

Verified: macOS three-stage fixpoint, ludic-dev test 135/135, and on Windows
the Mac-emitted Windows IR and the Windows-built compiler's IR are identical
through two generations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-13 01:59:41 +03:00
parent 572e09b2b1
commit 5801005fca
15 changed files with 23292 additions and 21806 deletions

View file

@ -1119,7 +1119,7 @@ function emit_call(e: Node) -> Val {
g_uses_panic = true
let m = emit_expr(e.kids[0])
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: panic: `)
let se = emit_bind("load ptr, ptr @__stderrp")
let se = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
g_term = true
@ -1134,7 +1134,7 @@ function emit_call(e: Node) -> Val {
emit(`{lbad}:\n`)
let m = emit_expr(e.kids[1])
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: assertion failed: `)
let se = emit_bind("load ptr, ptr @__stderrp")
let se = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
emit(`{lok}:\n`)

View file

@ -226,7 +226,7 @@ function emit_program() -> void {
if g_uses_noisert { emit_noise_prelude() } # @lp_noise_value2/perlin2/simplex2/fbm2/cellular2 (Q16.16)
if g_uses_logrt { emit_log_prelude() } # @L_log_level + @lp_log_emit (levelled stderr sink)
if g_uses_osrt { emit_os_prelude() } # @lp_os_args/platform/arch/save_dir/... (libc env + uname)
if g_uses_pid { emith("declare i32 @getpid()\n") }
if g_uses_pid and (not g_target_win) { emith("declare i32 @getpid()\n") }
if g_uses_unicodert { emit_unicode_prelude() } # @lp_uni_len/valid/decode/case/truncate/grapheme (UTF-8)
if g_uses_fsrt { emit_fs_prelude() } # @fn_fs_*/fn_path_*/fn_mime_* (libc + string ops)
if g_uses_pak { emit_pak_prelude() } # @lp_pak_* asset packs + the pre-main mount ctor

View file

@ -80,12 +80,16 @@ function emit_header() -> void {
emith("declare ptr @malloc(i64)\n")
emith("declare ptr @realloc(ptr, i64)\n")
emith("declare void @free(ptr)\n")
emith("declare ptr @fopen(ptr, ptr)\n")
emith("declare i64 @fread(ptr, i64, i64, ptr)\n")
emith("declare i64 @fwrite(ptr, i64, i64, ptr)\n")
emith("declare i32 @fseek(ptr, i64, i32)\n")
emith("declare i64 @ftell(ptr)\n")
emith("declare i32 @fclose(ptr)\n")
# on Windows these four are defined by emit_win_prelude, below
if not g_target_win {
emith("declare ptr @fopen(ptr, ptr)\n")
emith("declare i32 @fseek(ptr, i64, i32)\n")
emith("declare i64 @ftell(ptr)\n")
emith("declare i64 @time(ptr)\n")
}
emith("declare void @exit(i32)\n")
emith("declare i32 @system(ptr)\n")
emith("declare ptr @getenv(ptr)\n")
@ -98,7 +102,6 @@ function emit_header() -> void {
emith("declare i32 @atoi(ptr)\n")
emith("declare i32 @getchar()\n")
emith("declare i32 @putchar(i32)\n")
emith("declare i64 @time(ptr)\n")
emith("declare void @win_open(i32, i32, i32, ptr)\n")
emith("declare i32 @win_poll()\n")
emith("declare void @win_present(ptr, i32, i32)\n")
@ -126,8 +129,11 @@ function emit_header() -> void {
emith("declare i32 @hs_blen(i32)\n")
emith("declare ptr @hs_header(i32, ptr)\n")
emith("declare void @hs_free(i32)\n")
emith("@__stderrp = external global ptr\n")
emith("@__stdoutp = external global ptr\n")
if g_target_win { emit_win_prelude() } # the UCRT/Win32 definitions of the POSIX names above
else {
emith("@__stderrp = external global ptr\n")
emith("@__stdoutp = external global ptr\n")
}
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")
emith("@.fmt_long = private unnamed_addr constant [6 x i8] c\"%lld\\0A\\00\"\n")
emith("@.fmt_line = private unnamed_addr constant [4 x i8] c\"%s\\0A\\00\"\n")

View file

@ -88,8 +88,8 @@ function emit_intrinsic(name: pointer, e: Node) -> Val {
g_term = true
return val("0", "void")
}
if (name == "file_stderr") { return val(emit_bind("load ptr, ptr @__stderrp"), "pointer") }
if (name == "file_stdout") { return val(emit_bind("load ptr, ptr @__stdoutp"), "pointer") }
if (name == "file_stderr") { return val(emit_bind(stdstream_rhs(2)), "pointer") }
if (name == "file_stdout") { return val(emit_bind(stdstream_rhs(1)), "pointer") }
if (name == "run") {
let c = arg_code(e, 0)
return val(emit_bind(`call i32 @system(ptr {c})`), "int")

View file

@ -3,6 +3,15 @@
# Matches compiler/back/ir_intrin.c. Reached from emit_intrinsic's fall-through.
var g_windowed: bool = false # headless by default (games read stdin / dump PPM)
var g_target_win: bool = false # a Windows target: the header emits emit_win_prelude and the libSystem declares are skipped
# the IR that loads stdout (fd 1) or stderr (fd 2) as a FILE*. macOS names them as
# globals; the UCRT hands them out through a function (see emit_win.ludic).
function stdstream_rhs(fd: int) -> pointer {
if g_target_win { return `call ptr @lp_win_stdio(i32 {itoa(fd)})` }
if fd == 2 { return "load ptr, ptr @__stderrp" }
return "load ptr, ptr @__stdoutp"
}
function is_intrinsic2(name: pointer) -> bool {
if (name == "free") or (name == "fill") { return true }

View file

@ -91,13 +91,15 @@ function emit_mime_ns(meth: pointer, e: Node) -> Val {
# emit_fs_prelude — the Fs/Path/Mime runtime, emitted once per program that uses
# any of the three namespaces (g_uses_fsrt). Pure libc + byte IR.
function emit_fs_prelude() -> void {
emith("declare i32 @access(ptr, i32)\n")
emith("declare i32 @mkdir(ptr, i32)\n")
emith("declare i32 @remove(ptr)\n")
emith("declare i32 @rename(ptr, ptr)\n")
emith("declare ptr @opendir(ptr)\n")
emith("declare ptr @readdir(ptr)\n")
emith("declare i32 @closedir(ptr)\n")
if not g_target_win { # defined by emit_win_prelude on Windows
emith("declare i32 @access(ptr, i32)\n")
emith("declare i32 @mkdir(ptr, i32)\n")
emith("declare i32 @rename(ptr, ptr)\n")
emith("declare ptr @opendir(ptr)\n")
emith("declare ptr @readdir(ptr)\n")
emith("declare i32 @closedir(ptr)\n")
}
emit_fs_common()
emit_fs_path()
emit_fs_io()

View file

@ -81,7 +81,7 @@ function emit_log_prelude() -> void {
emith("@.log_nl = private unnamed_addr constant [2 x i8] c\"\\0A\\00\"\n")
emith("define void @lp_log_emit(i32 %lvl, ptr %s) {\n")
emith("entry:\n %th = load i32, ptr @L_log_level\n %skip = icmp slt i32 %lvl, %th\n br i1 %skip, label %done, label %go\n")
emith("go:\n %e = load ptr, ptr @__stderrp\n %n = call i64 @strlen(ptr %s)\n")
emith(`go:\n %e = {stdstream_rhs(2)}\n %n = call i64 @strlen(ptr %s)\n`)
emith(" %w = call i64 @fwrite(ptr %s, i64 1, i64 %n, ptr %e)\n %w2 = call i64 @fwrite(ptr @.log_nl, i64 1, i64 1, ptr %e)\n br label %done\n")
emith("done:\n ret void\n}\n")
}

View file

@ -71,10 +71,10 @@ function emit_os_ns(meth: pointer, e: Node) -> Val {
return val(emit_bind(`zext i1 {ne} to i32`), "bool")
}
if (meth == "stdout_write") or (meth == "stderr_write") {
var strm = "@__stdoutp"
if (meth == "stderr_write") { strm = "@__stderrp" }
var fd = 1
if (meth == "stderr_write") { fd = 2 }
let s = emit_expr(e.kids[0])
let f = emit_bind(`load ptr, ptr {strm}`)
let f = emit_bind(stdstream_rhs(fd))
let n = emit_bind(`call i64 @strlen(ptr {s.code})`)
emit(` call i64 @fwrite(ptr {s.code}, i64 1, i64 {n}, ptr {f})\n`)
return val("0", "void")
@ -120,9 +120,11 @@ function emit_os_ns(meth: pointer, e: Node) -> Val {
# prelude-backed Os.* methods (g_uses_osrt). Pure libc over NUL-terminated
# strings; declares the few POSIX symbols the header does not already carry.
function emit_os_prelude() -> void {
emith("declare i32 @setenv(ptr, ptr, i32)\n")
emith("declare i32 @unsetenv(ptr)\n")
emith("declare i32 @uname(ptr)\n")
if not g_target_win { # defined by emit_win_prelude on Windows
emith("declare i32 @setenv(ptr, ptr, i32)\n")
emith("declare i32 @unsetenv(ptr)\n")
emith("declare i32 @uname(ptr)\n")
}
# string constants (escaped + length-counted by emit_str_const)
let k_home = emit_str_const("HOME")
@ -161,6 +163,9 @@ function emit_os_prelude() -> void {
emith("define ptr @lp_os_join3(ptr %a, ptr %b, ptr %c) {\n")
emith("entry:\n %ab = call ptr @lp_os_join2(ptr %a, ptr %b)\n %r = call ptr @lp_os_join2(ptr %ab, ptr %c)\n ret ptr %r\n}\n")
# Windows has its own known folders and no uname; emit_win.ludic supplies them
if g_target_win { emit_os_win_known(k_dot, k_slash); return }
# the user's home directory, or "." when HOME is unset
emith(`define ptr @lp_os_home() {{\n %r = call ptr @lp_os_getenv_or(ptr {k_home}, ptr {k_dot})\n ret ptr %r\n}}\n`)

View file

@ -90,13 +90,15 @@ function use_pak() -> void { g_uses_pak = true; g_uses_fsrt = true }
# emit_pak_prelude — emitted once per program that opens a file, straight after
# the Fs prelude whose helpers it calls.
function emit_pak_prelude() -> void {
emith("declare i32 @open(ptr, i32, ...)\n")
emith("declare i32 @close(i32)\n")
emith("declare i64 @lseek(i32, i64, i32)\n")
emith("declare ptr @mmap(ptr, i64, i32, i32, i32, i64)\n")
emith("declare ptr @fmemopen(ptr, i64, ptr)\n")
emith("declare i32 @chdir(ptr)\n")
emith("declare i32 @_NSGetExecutablePath(ptr, ptr)\n")
if not g_target_win { # defined by emit_win_prelude on Windows
emith("declare i32 @open(ptr, i32, ...)\n")
emith("declare i32 @close(i32)\n")
emith("declare i64 @lseek(i32, i64, i32)\n")
emith("declare ptr @mmap(ptr, i64, i32, i32, i32, i64)\n")
emith("declare ptr @fmemopen(ptr, i64, ptr)\n")
emith("declare i32 @chdir(ptr)\n")
emith("declare i32 @_NSGetExecutablePath(ptr, ptr)\n")
}
emith("@.pak_rb = private unnamed_addr constant [3 x i8] c\"rb\\00\"\n")
emith("@.pak_idx = private unnamed_addr constant [12 x i8] c\"packs.index\\00\"\n")
@ -268,12 +270,19 @@ function emit_pak_boot() -> void {
# point the process at ~/Library/Application Support/<name>, creating it. A
# bundled game starts with its working directory at "/", so without this every
# relative write — every save — would fail.
# relative write — every save — would fail. On Windows the place is
# %APPDATA%\<name>, the same directory Os.save_dir names there.
emith("define void @lp_pak_sethome(ptr %name) {\n")
emith("entry:\n %h = call ptr @getenv(ptr @.pak_home)\n %nh = icmp eq ptr %h, null\n br i1 %nh, label %out, label %go\n")
emith("go:\n %a = call ptr @lp_path_join(ptr %h, ptr @.pak_appsup)\n %b = call ptr @lp_path_join(ptr %a, ptr %name)\n")
if g_target_win {
emith("entry:\n %h = call ptr @getenv(ptr @.pak_appdata)\n %nh = icmp eq ptr %h, null\n br i1 %nh, label %out, label %go\n")
emith("go:\n %b = call ptr @lp_path_join(ptr %h, ptr %name)\n call void @lp_win_slashes(ptr %b)\n")
} else {
emith("entry:\n %h = call ptr @getenv(ptr @.pak_home)\n %nh = icmp eq ptr %h, null\n br i1 %nh, label %out, label %go\n")
emith("go:\n %a = call ptr @lp_path_join(ptr %h, ptr @.pak_appsup)\n %b = call ptr @lp_path_join(ptr %a, ptr %name)\n")
}
emith(" %mk = call i32 @mkdir(ptr %b, i32 493)\n %cd = call i32 @chdir(ptr %b)\n br label %out\n")
emith("out:\n ret void\n}\n")
emith("@.pak_appdata = private unnamed_addr constant [8 x i8] c\"APPDATA\\00\"\n")
# The constructor. Look beside the executable: ../Resources first, which is
# where the bundler puts things inside a .app, then the executable's own

View file

@ -0,0 +1,244 @@
# emit_win.ludic — the Windows target's libc surface, in LLVM IR.
#
# Everything the rest of the backend emits calls POSIX by its POSIX name: fopen,
# ftell, rename, opendir, mmap, fmemopen, _NSGetExecutablePath. On macOS those
# are libSystem. On Windows most of them either do not exist or exist with a
# different contract, and the dangerous ones are the second kind, because they
# link and run and are wrong:
#
# rename fails when the target exists. Fs.write_text saves by writing <p>.tmp
# and renaming it over <p>, so every save after the first would fail.
# ftell returns a 32-bit long; the IR reads i64, so the top half is garbage.
# fopen "w" and "r" are TEXT mode: \n becomes \r\n on the way out and a
# byte 26 ends a read. A save file or a PNG opened "r" is corrupted.
# stdout text mode too, so IR written to stdout (the reseed contract) grows
# a \r on every line.
#
# So on --target *windows* the header emits this prelude, which DEFINES those
# names as internal functions over the UCRT and Win32, and the declare lines that
# would name the libSystem versions are skipped at their call sites. Nothing else
# in the backend changes: a `call ptr @fopen` binds to the definition here.
#
# The contracts reproduced are exactly the ones the backend relies on, not the
# whole of POSIX:
#
# readdir returns a pointer whose d_name is at offset 21, the macOS dirent
# layout emit_fs reads. The pointer is placed 21 bytes before the
# WIN32_FIND_DATAA cFileName, so there is no copy.
# mmap read-only whole-file mapping of a descriptor (the pack mount).
# fmemopen a read-only FILE* over bytes. The UCRT has no memory stream, so the
# bytes go to a delete-on-close temporary file, which the cache keeps
# in memory. A packed asset costs one copy per open on Windows.
# uname "Windows" in field 0 and the binary's arch at offset 1024.
#
# Paths come back from Windows with backslashes; every path this prelude hands to
# the rest of the runtime (the executable path, the known folders) is rewritten to
# forward slashes, which Windows accepts everywhere, so Path.* keeps splitting on
# '/' alone.
function emit_win_prelude() -> void {
emith("declare ptr @__acrt_iob_func(i32)\n")
emith("declare i32 @_setmode(i32, i32)\n")
emith("declare ptr @_fsopen(ptr, ptr, i32)\n")
emith("declare i64 @_ftelli64(ptr)\n")
emith("declare i32 @_fseeki64(ptr, i64, i32)\n")
emith("declare i64 @_time64(ptr)\n")
emith("declare i32 @_access(ptr, i32)\n")
emith("declare i32 @_mkdir(ptr)\n")
emith("declare i32 @_chdir(ptr)\n")
emith("declare i32 @_getpid()\n")
emith("declare i32 @_open(ptr, i32, ...)\n")
emith("declare i32 @_close(i32)\n")
emith("declare i64 @_lseeki64(i32, i64, i32)\n")
emith("declare i32 @_putenv_s(ptr, ptr)\n")
emith("declare i64 @_get_osfhandle(i32)\n")
emith("declare i32 @_open_osfhandle(i64, i32)\n")
emith("declare ptr @_fdopen(i32, ptr)\n")
emith("declare i32 @MoveFileExA(ptr, ptr, i32)\n")
emith("declare ptr @FindFirstFileA(ptr, ptr)\n")
emith("declare i32 @FindNextFileA(ptr, ptr)\n")
emith("declare i32 @FindClose(ptr)\n")
emith("declare ptr @CreateFileMappingA(ptr, ptr, i32, i32, i32, ptr)\n")
emith("declare ptr @MapViewOfFile(ptr, i32, i32, i32, i64)\n")
emith("declare i32 @CloseHandle(ptr)\n")
emith("declare i32 @GetModuleFileNameA(ptr, ptr, i32)\n")
emith("declare i32 @GetTempPathA(i32, ptr)\n")
emith("declare i32 @GetTempFileNameA(ptr, ptr, i32, ptr)\n")
emith("declare ptr @CreateFileA(ptr, i32, i32, ptr, i32, i32, ptr)\n")
emith("declare i32 @WriteFile(ptr, ptr, i32, ptr, ptr)\n")
emith("declare i32 @SetFilePointer(ptr, i32, ptr, i32)\n")
emith("@.win_rb = private unnamed_addr constant [3 x i8] c\"rb\\00\"\n")
emith("@.win_lpk = private unnamed_addr constant [4 x i8] c\"lpk\\00\"\n")
emith("@.win_empty = private unnamed_addr constant [1 x i8] c\"\\00\"\n")
emith("@.win_windows = private unnamed_addr constant [8 x i8] c\"Windows\\00\"\n")
emith("@.win_x64 = private unnamed_addr constant [7 x i8] c\"x86_64\\00\"\n")
emit_win_stdio()
emit_win_files()
emit_win_dirs()
emit_win_maps()
emit_win_process()
}
# ---- paths and the standard streams -------------------------------------------
function emit_win_stdio() -> void {
# '\' -> '/' in place, over a buffer this runtime owns
emith("define internal void @lp_win_slashes(ptr %s) {\n")
emith("entry:\n br label %lp\n")
emith("lp:\n %p = phi ptr [ %s, %entry ], [ %p1, %nx ]\n %c = load i8, ptr %p\n %z = icmp eq i8 %c, 0\n br i1 %z, label %done, label %chk\n")
emith("chk:\n %bs = icmp eq i8 %c, 92\n br i1 %bs, label %fix, label %nx\n")
emith("fix:\n store i8 47, ptr %p\n br label %nx\n")
emith("nx:\n %p1 = getelementptr i8, ptr %p, i64 1\n br label %lp\n")
emith("done:\n ret void\n}\n")
# stdout/stderr as FILE*, switched to binary on first use so IR and JSON written
# through them keep their bytes (see the header note)
emith("@L_win_stdio_bin = internal global i32 0\n")
emith("define internal ptr @lp_win_stdio(i32 %fd) {\n")
emith("entry:\n %b = load i32, ptr @L_win_stdio_bin\n %done = icmp ne i32 %b, 0\n br i1 %done, label %get, label %set\n")
emith("set:\n store i32 1, ptr @L_win_stdio_bin\n %s1 = call i32 @_setmode(i32 1, i32 32768)\n %s2 = call i32 @_setmode(i32 2, i32 32768)\n br label %get\n")
emith("get:\n %f = call ptr @__acrt_iob_func(i32 %fd)\n ret ptr %f\n}\n")
}
# ---- files --------------------------------------------------------------------
function emit_win_files() -> void {
# fopen: always binary (append 'b' when the mode lacks one), and shareable, which
# fopen_s is not - a save being read while another handle writes it must open
emith("define internal ptr @fopen(ptr %p, ptr %m) {\n")
emith("entry:\n %buf = alloca [8 x i8]\n %ip = alloca i32\n %hb = alloca i32\n store i32 0, ptr %ip\n store i32 0, ptr %hb\n br label %lp\n")
emith("lp:\n %i = load i32, ptr %ip\n %src = getelementptr i8, ptr %m, i32 %i\n %c = load i8, ptr %src\n %z = icmp eq i8 %c, 0\n %full = icmp sge i32 %i, 6\n %stop = or i1 %z, %full\n br i1 %stop, label %fin, label %cp\n")
emith("cp:\n %dst = getelementptr i8, ptr %buf, i32 %i\n store i8 %c, ptr %dst\n %isb = icmp eq i8 %c, 98\n %hbv = load i32, ptr %hb\n %hbn = select i1 %isb, i32 1, i32 %hbv\n store i32 %hbn, ptr %hb\n %i1 = add i32 %i, 1\n store i32 %i1, ptr %ip\n br label %lp\n")
emith("fin:\n %hbf = load i32, ptr %hb\n %need = icmp eq i32 %hbf, 0\n br i1 %need, label %addb, label %term\n")
emith("addb:\n %bp = getelementptr i8, ptr %buf, i32 %i\n store i8 98, ptr %bp\n %i2 = add i32 %i, 1\n store i32 %i2, ptr %ip\n br label %term\n")
# _SH_DENYNO = 0x40
emith("term:\n %ie = load i32, ptr %ip\n %ep = getelementptr i8, ptr %buf, i32 %ie\n store i8 0, ptr %ep\n %f = call ptr @_fsopen(ptr %p, ptr %buf, i32 64)\n ret ptr %f\n}\n")
emith("define internal i64 @ftell(ptr %f) {\nentry:\n %r = call i64 @_ftelli64(ptr %f)\n ret i64 %r\n}\n")
emith("define internal i32 @fseek(ptr %f, i64 %o, i32 %w) {\nentry:\n %r = call i32 @_fseeki64(ptr %f, i64 %o, i32 %w)\n ret i32 %r\n}\n")
# rename over an existing file: MOVEFILE_REPLACE_EXISTING | COPY_ALLOWED | WRITE_THROUGH
emith("define internal i32 @rename(ptr %a, ptr %b) {\nentry:\n %r = call i32 @MoveFileExA(ptr %a, ptr %b, i32 11)\n %ok = icmp ne i32 %r, 0\n %v = select i1 %ok, i32 0, i32 -1\n ret i32 %v\n}\n")
emith("define internal i32 @access(ptr %p, i32 %m) {\nentry:\n %r = call i32 @_access(ptr %p, i32 %m)\n ret i32 %r\n}\n")
emith("define internal i32 @mkdir(ptr %p, i32 %mode) {\nentry:\n %r = call i32 @_mkdir(ptr %p)\n ret i32 %r\n}\n")
# O_RDONLY is 0 on both; _O_BINARY = 0x8000
emith("define internal i32 @open(ptr %p, i32 %fl, ...) {\nentry:\n %f2 = or i32 %fl, 32768\n %r = call i32 (ptr, i32, ...) @_open(ptr %p, i32 %f2)\n ret i32 %r\n}\n")
emith("define internal i32 @close(i32 %fd) {\nentry:\n %r = call i32 @_close(i32 %fd)\n ret i32 %r\n}\n")
emith("define internal i64 @lseek(i32 %fd, i64 %o, i32 %w) {\nentry:\n %r = call i64 @_lseeki64(i32 %fd, i64 %o, i32 %w)\n ret i64 %r\n}\n")
}
# ---- directories: opendir / readdir / closedir over FindFirstFileA --------------
#
# DIR is one 368-byte block: the find handle at 0, a "first entry pending" flag at
# 8, and WIN32_FIND_DATAA at 16, whose cFileName sits at 16 + 44 = 60. readdir
# returns block + 39 so that d_name, read at +21, is cFileName itself.
function emit_win_dirs() -> void {
emith("define internal ptr @opendir(ptr %path) {\n")
emith("entry:\n %l = call i64 @strlen(ptr %path)\n %cap = add i64 %l, 3\n %pat = call ptr @malloc(i64 %cap)\n call ptr @memcpy(ptr %pat, ptr %path, i64 %l)\n")
emith(" %e0 = getelementptr i8, ptr %pat, i64 %l\n store i8 47, ptr %e0\n %e1 = getelementptr i8, ptr %e0, i64 1\n store i8 42, ptr %e1\n %e2 = getelementptr i8, ptr %e0, i64 2\n store i8 0, ptr %e2\n")
emith(" %d = call ptr @malloc(i64 368)\n %fd = getelementptr i8, ptr %d, i64 16\n %h = call ptr @FindFirstFileA(ptr %pat, ptr %fd)\n call void @free(ptr %pat)\n")
emith(" %bad = icmp eq ptr %h, inttoptr (i64 -1 to ptr)\n br i1 %bad, label %no, label %ok\n")
emith("no:\n call void @free(ptr %d)\n ret ptr null\n")
emith("ok:\n store ptr %h, ptr %d\n %fl = getelementptr i8, ptr %d, i64 8\n store i32 1, ptr %fl\n ret ptr %d\n}\n")
emith("define internal ptr @readdir(ptr %d) {\n")
emith("entry:\n %fl = getelementptr i8, ptr %d, i64 8\n %f = load i32, ptr %fl\n %first = icmp ne i32 %f, 0\n br i1 %first, label %take, label %next\n")
emith("take:\n store i32 0, ptr %fl\n br label %give\n")
emith("next:\n %h = load ptr, ptr %d\n %fd = getelementptr i8, ptr %d, i64 16\n %ok = call i32 @FindNextFileA(ptr %h, ptr %fd)\n %more = icmp ne i32 %ok, 0\n br i1 %more, label %give, label %end\n")
emith("give:\n %de = getelementptr i8, ptr %d, i64 39\n ret ptr %de\n")
emith("end:\n ret ptr null\n}\n")
emith("define internal i32 @closedir(ptr %d) {\nentry:\n %h = load ptr, ptr %d\n %r = call i32 @FindClose(ptr %h)\n call void @free(ptr %d)\n ret i32 0\n}\n")
}
# ---- mapped files and memory streams -------------------------------------------
function emit_win_maps() -> void {
# mmap(null, len, PROT_READ, MAP_PRIVATE, fd, 0): PAGE_READONLY = 2, FILE_MAP_READ = 4.
# The view outlives both the mapping handle and the descriptor.
emith("define internal ptr @mmap(ptr %a, i64 %len, i32 %prot, i32 %flags, i32 %fd, i64 %off) {\n")
emith("entry:\n %oh = call i64 @_get_osfhandle(i32 %fd)\n %h = inttoptr i64 %oh to ptr\n %m = call ptr @CreateFileMappingA(ptr %h, ptr null, i32 2, i32 0, i32 0, ptr null)\n")
emith(" %nm = icmp eq ptr %m, null\n br i1 %nm, label %bad, label %map\n")
emith("map:\n %v = call ptr @MapViewOfFile(ptr %m, i32 4, i32 0, i32 0, i64 0)\n %c = call i32 @CloseHandle(ptr %m)\n %nv = icmp eq ptr %v, null\n br i1 %nv, label %bad, label %ok\n")
emith("ok:\n ret ptr %v\n")
emith("bad:\n ret ptr inttoptr (i64 -1 to ptr)\n}\n")
# fmemopen(buf, len, "rb") -> a delete-on-close temporary holding the bytes.
# GENERIC_READ|GENERIC_WRITE = 0xC0000000, share READ|WRITE|DELETE = 7,
# CREATE_ALWAYS = 2, FILE_ATTRIBUTE_TEMPORARY|FILE_FLAG_DELETE_ON_CLOSE = 0x04000100.
emith("define internal ptr @fmemopen(ptr %buf, i64 %len, ptr %mode) {\n")
emith("entry:\n %dir = alloca [270 x i8]\n %name = alloca [270 x i8]\n %wr = alloca i32\n %n = call i32 @GetTempPathA(i32 260, ptr %dir)\n %z = icmp eq i32 %n, 0\n br i1 %z, label %bad, label %mk\n")
emith("mk:\n %u = call i32 @GetTempFileNameA(ptr %dir, ptr @.win_lpk, i32 0, ptr %name)\n %uz = icmp eq i32 %u, 0\n br i1 %uz, label %bad, label %cr\n")
emith("cr:\n %h = call ptr @CreateFileA(ptr %name, i32 -1073741824, i32 7, ptr null, i32 2, i32 67109120, ptr null)\n %hb = icmp eq ptr %h, inttoptr (i64 -1 to ptr)\n br i1 %hb, label %bad, label %wrt\n")
emith("wrt:\n %l32 = trunc i64 %len to i32\n %w = call i32 @WriteFile(ptr %h, ptr %buf, i32 %l32, ptr %wr, ptr null)\n %sp = call i32 @SetFilePointer(ptr %h, i32 0, ptr null, i32 0)\n")
emith(" %hi = ptrtoint ptr %h to i64\n %fd = call i32 @_open_osfhandle(i64 %hi, i32 32768)\n %fdb = icmp slt i32 %fd, 0\n br i1 %fdb, label %shut, label %fdo\n")
emith("fdo:\n %f = call ptr @_fdopen(i32 %fd, ptr @.win_rb)\n ret ptr %f\n")
emith("shut:\n %c = call i32 @CloseHandle(ptr %h)\n br label %bad\n")
emith("bad:\n ret ptr null\n}\n")
}
# ---- the process ----------------------------------------------------------------
function emit_win_process() -> void {
emith("define internal i32 @_NSGetExecutablePath(ptr %buf, ptr %szp) {\n")
emith("entry:\n %sz = load i32, ptr %szp\n %n = call i32 @GetModuleFileNameA(ptr null, ptr %buf, i32 %sz)\n %bad = icmp eq i32 %n, 0\n %full = icmp sge i32 %n, %sz\n %no = or i1 %bad, %full\n br i1 %no, label %fail, label %fix\n")
emith("fix:\n call void @lp_win_slashes(ptr %buf)\n ret i32 0\n")
emith("fail:\n ret i32 -1\n}\n")
emith("define internal i32 @chdir(ptr %p) {\nentry:\n %r = call i32 @_chdir(ptr %p)\n ret i32 %r\n}\n")
emith("define internal i32 @getpid() {\nentry:\n %r = call i32 @_getpid()\n ret i32 %r\n}\n")
emith("define internal i64 @time(ptr %t) {\nentry:\n %r = call i64 @_time64(ptr %t)\n ret i64 %r\n}\n")
emith("define internal i32 @setenv(ptr %n, ptr %v, i32 %o) {\nentry:\n %r = call i32 @_putenv_s(ptr %n, ptr %v)\n ret i32 %r\n}\n")
# an empty value removes the variable
emith("define internal i32 @unsetenv(ptr %n) {\nentry:\n %r = call i32 @_putenv_s(ptr %n, ptr @.win_empty)\n ret i32 %r\n}\n")
# uname: sysname at 0, machine at 1024 (the BSD utsname offsets emit_os reads)
emith("define internal i32 @uname(ptr %buf) {\n")
emith("entry:\n call ptr @memcpy(ptr %buf, ptr @.win_windows, i64 8)\n %m = getelementptr i8, ptr %buf, i64 1024\n call ptr @memcpy(ptr %m, ptr @.win_x64, i64 7)\n ret i32 0\n}\n")
}
# ---- Os.* known folders ----------------------------------------------------------
#
# Called from emit_os_prelude in place of the macOS/XDG half, after the string
# helpers it shares (lp_os_getenv_or, lp_os_join2, lp_os_join3) are emitted.
#
# save/config %APPDATA%\<app> roaming, per-user: where a PC game's save goes
# cache %LOCALAPPDATA%\<app> machine-local, never roams
# temp %TEMP%
# home %USERPROFILE%
#
# Every one comes back as a fresh copy with forward slashes. A variable that is
# unset falls back to the profile directory rather than the working directory.
function emit_os_win_known(k_dot: pointer, k_slash: pointer) -> void {
let k_uprof = emit_str_const("USERPROFILE")
let k_appdata = emit_str_const("APPDATA")
let k_localad = emit_str_const("LOCALAPPDATA")
let k_temp = emit_str_const("TEMP")
let k_empty = emit_str_const("")
let k_windows = emit_str_const("windows")
let k_x64 = emit_str_const("x86_64")
emith(`define ptr @lp_os_home() {{\n %r = call ptr @lp_os_getenv_or(ptr {k_uprof}, ptr {k_dot})\n ret ptr %r\n}}\n`)
emith("define ptr @lp_os_winknown(ptr %var, ptr %app) {\n")
emith("entry:\n %v = call ptr @getenv(ptr %var)\n %z = icmp eq ptr %v, null\n br i1 %z, label %home, label %use\n")
emith("home:\n %h = call ptr @lp_os_home()\n br label %join\n")
emith("use:\n br label %join\n")
emith(`join:\n %b = phi ptr [ %h, %home ], [ %v, %use ]\n %r = call ptr @lp_os_join3(ptr %b, ptr {k_slash}, ptr %app)\n call void @lp_win_slashes(ptr %r)\n ret ptr %r\n}}\n`)
emith(`define ptr @lp_os_save_dir(ptr %app) {{\n %r = call ptr @lp_os_winknown(ptr {k_appdata}, ptr %app)\n ret ptr %r\n}}\n`)
emith(`define ptr @lp_os_config_dir(ptr %app) {{\n %r = call ptr @lp_os_winknown(ptr {k_appdata}, ptr %app)\n ret ptr %r\n}}\n`)
emith(`define ptr @lp_os_cache_dir(ptr %app) {{\n %r = call ptr @lp_os_winknown(ptr {k_localad}, ptr %app)\n ret ptr %r\n}}\n`)
emith(`define ptr @lp_os_temp_dir() {{\n %t = call ptr @lp_os_getenv_or(ptr {k_temp}, ptr {k_dot})\n %r = call ptr @lp_os_join2(ptr %t, ptr {k_empty})\n call void @lp_win_slashes(ptr %r)\n ret ptr %r\n}}\n`)
emith(`define ptr @lp_os_platform() {{\n ret ptr {k_windows}\n}}\n`)
emith(`define ptr @lp_os_arch() {{\n ret ptr {k_x64}\n}}\n`)
# Os.args(), as emit_os_prelude defines it
emith("define ptr @lp_os_args() {\n")
emith("entry:\n %c = load i32, ptr @L_argc\n %v = load ptr, ptr @L_argv\n")
emith(" %h = call ptr @malloc(i64 16)\n")
emith(" %d0 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 0\n store ptr %v, ptr %d0\n")
emith(" %d1 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 1\n store i32 %c, ptr %d1\n")
emith(" %d2 = getelementptr inbounds %LSlice, ptr %h, i32 0, i32 2\n store i32 %c, ptr %d2\n")
emith(" ret ptr %h\n}\n")
}