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")
}

View file

@ -543,16 +543,39 @@ function parse_namespace() -> void {
}
}
# directory part of a path, including the trailing '/', or "" if none
# Is the compiler itself running on Windows? A property of the HOST, asked at run
# time - every Windows process inherits OS=Windows_NT - so one compiler source
# answers correctly whichever platform its binary was built for.
function host_is_windows() -> bool {
let os = getenv("OS")
if (os == null) { return false }
return os == "Windows_NT"
}
# a path separator: '/' everywhere, '\' too on Windows (argv[0] and %PATH% use it)
function is_sep(c: int) -> bool { return (c == '/') or (c == 92) }
# a copy of `s` with every '\' as '/', which Windows accepts wherever it takes a path
function fwd_slashes(s: pointer) -> pointer {
let n = len(s)
let out = bytes(n + 1)
var i = 0
while i < n { if s[i] == 92 { out[i] = '/' } else { out[i] = s[i] }; i += 1 }
out[n] = 0
return out
}
# directory part of a path, including the trailing separator, or "" if none
function dir_of(path: pointer) -> pointer {
var last = -1
var i = 0
while path[i] != 0 { if path[i] == '/' { last = i }; i += 1 }
while path[i] != 0 { if is_sep(path[i]) { last = i }; i += 1 }
if last < 0 { return "" }
return path[0..last + 1]
}
function join_path(dir: pointer, rel: pointer) -> pointer {
if rel[0] == '/' { return rel } # absolute
if is_sep(rel[0]) { return rel } # absolute
if rel[0] != 0 { if rel[1] == ':' { return rel } } # absolute: a Windows drive
return dir + rel
}
@ -569,30 +592,41 @@ function join_path(dir: pointer, rel: pointer) -> pointer {
# wants. Invoked through $PATH argv[0] carries no directory at all, so the PATH
# entries are searched for the binary first — an installed `ludicc mygame.ludic`
# has to find its own runtime with nothing in the environment.
#
# On Windows argv[0] and $PATH carry backslashes, $PATH is split on ';', and a
# binary found through it is `ludicc.exe` while argv[0] may say `ludicc`, so the
# directory is read with forward slashes and the lookup tries both names.
function ludic_home() -> pointer {
let env = getenv("LUDIC_HOME")
if (env != null) { return ensure_slash(env) }
var d = dir_of(arg(0))
if (env != null) { return ensure_slash(fwd_slashes(env)) }
var d = fwd_slashes(dir_of(arg(0)))
if (len(d) == 0) { d = path_lookup_dir(base_name(arg(0))) }
if (len(d) == 0) { return "" }
if (len(d) >= 4) and (d[len(d) - 4..len(d)] == "bin/") { return d[0..len(d) - 4] }
return d
}
# the first $PATH entry holding an openable file called `name`, with a trailing
# '/' — "" when $PATH is unset or nothing matches.
# the first $PATH entry holding an openable file called `name` (or `name`.exe on
# Windows), with a trailing '/' — "" when $PATH is unset or nothing matches.
function path_lookup_dir(name: pointer) -> pointer {
let p = getenv("PATH")
if (p == null) { return "" }
let win = host_is_windows()
var sep = ':'
if win { sep = ';' }
let n = len(p)
var start = 0
var i = 0
while i <= n {
if (i == n) or (p[i] == ':') {
if (i == n) or (p[i] == sep) {
if i > start {
let dir = ensure_slash(p[start..i])
let dir = ensure_slash(fwd_slashes(p[start..i]))
let f = file_open(dir + name, "rb")
if (f != null) { file_close(f); return dir }
if win {
let fx = file_open(dir + name + ".exe", "rb")
if (fx != null) { file_close(fx); return dir }
}
}
start = i + 1
}

File diff suppressed because it is too large Load diff

View file

@ -9,17 +9,24 @@
# ludicc app.ludic -o bin/app --headless # force headless
# ludicc app.ludic --emit-llvm -o app.ll # stop at the IR
# ludicc app.ludic --run # compile to a temp binary and run it
# ludicc app.ludic --target x86_64-pc-windows-msvc --emit-llvm -o app.ll
# # IR for another platform
#
# This is the compiler, not the command line a user of the language meets: that
# is `ludic` (tools/ludic-cli), which drives this. With no -o and no --run, IR
# still goes to stdout — the contract `ludic-dev reseed` and `ludic dev
# bootstrap` rely on, so the bootstrap is untouched.
#
# The target is the host unless --target names another. Only its platform is
# read, and only to choose between two runtimes: macOS (libSystem, Cocoa) and
# Windows (the UCRT and Win32, emit_win.ludic). The IR carries no triple either
# way; clang assembles it for the machine it runs on.
# basename: the part of a path after the last '/'.
# basename: the part of a path after the last separator.
function base_name(path: pointer) -> pointer {
var last = -1
var i = 0
while path[i] != 0 { if path[i] == '/' { last = i }; i += 1 }
while path[i] != 0 { if is_sep(path[i]) { last = i }; i += 1 }
return path[last + 1..i]
}
@ -59,6 +66,56 @@ function chomp(s: pointer) -> pointer {
return s[0..n]
}
# does `s` contain `sub`?
function has_sub(s: pointer, sub: pointer) -> bool {
let n = len(s); let m = len(sub)
var i = 0
while i + m <= n {
if s[i..i + m] == sub { return true }
i += 1
}
return false
}
# ---- the host shell ------------------------------------------------------------
# run() is the C library's system(), which is /bin/sh on macOS and cmd.exe on
# Windows. The compiler asks the shell for three things only - make a directory,
# delete a file, run a program - and says each in the host's own words.
# a copy of `p` with every '/' as '\', for cmd.exe, which reads '/' as a switch
function win_path(p: pointer) -> pointer {
let n = len(p)
let out = bytes(n + 1)
var i = 0
while i < n { if p[i] == '/' { out[i] = 92 } else { out[i] = p[i] }; i += 1 }
out[n] = 0
return out
}
function make_dir(d: pointer) -> void {
if host_is_windows() { let w = win_path(d); run(`if not exist "{w}" mkdir "{w}"`) }
else { run(`mkdir -p {d}`) }
}
function remove_file(p: pointer) -> void {
if host_is_windows() { run(`del /f /q "{win_path(p)}" 2>nul`) }
else { run(`rm -f {p}`) }
}
# the clang that assembles and links: $LUDIC_CC, else `clang` from $PATH - or, on a
# Windows host, the LLVM installer's own location when it is there, because that
# installer does not put clang on %PATH% unless asked to.
function find_cc() -> pointer {
let env = getenv("LUDIC_CC")
if (env != null) { return env }
if host_is_windows() {
let llvm = "C:/Program Files/LLVM/bin/clang.exe"
let f = file_open(llvm, "rb")
if (f != null) { file_close(f); return `"{llvm}"` }
}
return "clang"
}
# print "ludic <version>" and exit. The version is read at runtime from the
# VERSION file at the install root (the single source of truth a release bumps),
# so a version change never requires reseeding the compiler.
@ -77,6 +134,7 @@ entry {
var fmt = false # --fmt: lex + parse only, then exit (the doc-check gate)
var save = false # --save-temps: keep the intermediate .ll
var run = false # compile then execute the result
var target = null # --target <triple>: null means the host
g_coverage = false # --coverage: instrument each statement with a per-line hit counter
var ai = 1
@ -91,6 +149,7 @@ entry {
else if a == "--save-temps" { save = true }
else if a == "--run" { run = true }
else if a == "--coverage" { g_coverage = true }
else if a == "--target" { ai += 1; if ai < arg_count() { target = arg(ai) } }
else if a == "-o" { ai += 1; if ai < arg_count() { out = arg(ai) } }
else if a[0] == '-' {
# an unknown flag is ignored (with a note) rather than mistaken for the input file
@ -102,8 +161,11 @@ entry {
}
if (path == null) {
die("usage: ludicc <file.ludic> [-o out] [--windowed|--headless] [--emit-llvm] [--save-temps]\n")
die("usage: ludicc <file.ludic> [-o out] [--windowed|--headless] [--target triple] [--emit-llvm] [--save-temps]\n")
}
if (target != null) { g_target_win = has_sub(target, "windows") }
else { g_target_win = host_is_windows() }
let src = read_file(path)
if (src == null) { die("ludicc: cannot open input\n") }
@ -137,19 +199,36 @@ entry {
if emit_ir { ir_flush(out); return }
if not has_target { ir_flush(null); return }
# The Windows runtime is the libc surface so far: a window, audio, HTTP and GL
# are macOS platform layers with no Windows counterpart yet, and linking one
# would fail on a hundred Objective-C symbols rather than say so.
if g_target_win {
if g_windowed { die("ludicc: a windowed build is not available on Windows yet; build with --headless\n") }
if g_uses_gl { die("ludicc: Gl.* is not available on Windows yet\n") }
if g_uses_http { die("ludicc: Http.* is not available on Windows yet\n") }
}
# a run with no explicit -o lands in a temp file
if run and (out == null) {
out = `/tmp/ludic-run-{strip_ludic(base_name(path))}`
if host_is_windows() { out = `{fwd_slashes(getenv_or("TEMP", "."))}/ludic-run-{strip_ludic(base_name(path))}` }
else { out = `/tmp/ludic-run-{strip_ludic(base_name(path))}` }
}
# a Windows program is found and run by its extension
if g_target_win {
let n = len(out)
var has_exe = false
if n >= 4 { has_exe = (out[n - 4..n] == ".exe") }
if not has_exe { out = out + ".exe" }
}
# make the output directory if the user asked for e.g. bin/app
let odir = dir_of(out)
if len(odir) > 0 { run(`mkdir -p {odir}`) }
if len(odir) > 0 { make_dir(odir) }
let ll = `{out}.ll`
if not ir_flush(ll) { die("ludicc: cannot write IR\n") }
let cc = getenv_or("LUDIC_CC", "clang")
let cc = find_cc()
let home = ludic_home()
# clang twice: assemble the IR, then link. A windowed build adds the macOS
@ -190,11 +269,13 @@ entry {
cmd = `{cmd} -o {out}`
let rc = run(cmd)
if not save { run(`rm -f {ll}`) }
if not save { remove_file(ll) }
if rc != 0 { die("ludicc: link failed\n") }
# run mode: execute the binary we just built and forward its exit code
# run mode: execute the binary we just built and forward its exit code.
# system() hands back a wait status on POSIX and the exit code itself on Windows.
if run {
if host_is_windows() { exit(run(`"{win_path(out)}"`)) }
let st = run(out)
exit(((st >> 8) & 255))
}