The compiler runs on Windows (and has seeds for macOS)

Windows: the compiler made from bootstrap/windows-x86_64.ll runs under Wine, compiles itself to the same IR, builds an
exe of itself, and every test program passes through it. It needed: Windows-sized unsigned long for libclang's unsaved
file, no 'cross' when Windows builds for Windows, --target passed to libclang (libclang.dll assumes MSVC), the MinGW
triple always in the IR, FINCH_LDFLAGS, .exe names and FINCH_LLVM_INCLUDE in bootstrap/build.sh, winseed.sh/winbuild.sh.
macOS: seeds for arm64 and x86-64 made with the 11.3 SDK; IR is accepted by clang, nothing has run on a Mac yet.
CI gets a windows and a macos job. UNTESTED on real Windows/macOS runners.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
This commit is contained in:
Nexoniarz 2026-10-26 00:36:15 +01:00
commit b53e0910d8
18 changed files with 398686 additions and 45576 deletions

View file

@ -1,8 +1,8 @@
name: CI name: CI
# The compiler is written in Finch. It is built from bootstrap/<system>.ll, its own LLVM IR (see bootstrap/README.md), # The compiler is written in Finch. It is built from bootstrap/<system>.ll, its own LLVM IR (see bootstrap/README.md),
# then from itself. Linux x86-64 and ARM64 have seeds; macOS and Windows machines can't build the compiler yet # then from itself. Seeds: Linux x86-64 and ARM64 (tested here), Windows x86-64 (tested under Wine, and here),
# (programs for them are built from Linux with --target, and tested here under Wine and qemu). # macOS ARM64 and x86-64 (made with Apple's headers; only checked as IR until the macos job below passes).
on: on:
push: push:
@ -74,6 +74,49 @@ jobs:
FINCH_CC=aarch64-linux-gnu-gcc QEMU_LD_PREFIX=/usr/aarch64-linux-gnu tests/cross.sh arm64 FINCH_CC=aarch64-linux-gnu-gcc QEMU_LD_PREFIX=/usr/aarch64-linux-gnu tests/cross.sh arm64
fi fi
macos:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Install LLVM 21 (libclang)
run: brew install llvm@21 pkg-config
- name: Build the compiler
run: |
export FINCH_CLANG="$(brew --prefix llvm@21)/bin/clang"
export FINCH_LIBCLANG="-L$(brew --prefix llvm@21)/lib -lclang"
export FINCH_LLVM_INCLUDE="$(brew --prefix llvm@21)/include"
bootstrap/build.sh
build/finch version
- name: Tests
run: tests/run.sh
windows:
runs-on: windows-2022
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Download llvm-mingw (clang with MinGW headers) and LLVM for Windows (libclang.dll)
run: |
curl -sSL -o mingw.zip https://github.com/mstorsjo/llvm-mingw/releases/download/20251216/llvm-mingw-20251216-ucrt-x86_64.zip
7z x -y mingw.zip -o/c/ > /dev/null
curl -sSL -o llvm.exe https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.8/LLVM-21.1.8-win64.exe
7z x -y llvm.exe -o/c/llvm > /dev/null
- name: Build the compiler
run: |
export PATH="/c/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH"
export FINCH_LIBCLANG="C:/llvm/bin/libclang.dll"
export FINCH_LLVM_INCLUDE="C:/llvm/include"
export FINCH_LDFLAGS="-LC:/llvm/bin"
bootstrap/build.sh
build/finch.exe version
- name: Tests
run: |
export PATH="/c/llvm-mingw-20251216-ucrt-x86_64/bin:$PATH"
export FINCH_LDFLAGS="-LC:/llvm/bin"
FINCH=../build/finch.exe tests/run.sh
vscode: vscode:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
defaults: defaults:

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
build/ build/
*.o *.o
core.*

View file

@ -7,6 +7,8 @@ the compiler's own LLVM IR, one file per system:
|---|---|---| |---|---|---|
| `linux-x86_64.ll` | Linux x86-64 | built and tested here and in CI | | `linux-x86_64.ll` | Linux x86-64 | built and tested here and in CI |
| `linux-aarch64.ll` | Linux ARM64 | made with the ARM64 cross compiler; CI builds and tests it on an ARM64 runner | | `linux-aarch64.ll` | Linux ARM64 | made with the ARM64 cross compiler; CI builds and tests it on an ARM64 runner |
| `windows-x86_64.ll` | Windows x86-64 (MinGW: llvm-mingw's clang + libclang.dll) | built and run under Wine: it compiles itself, and every test program runs |
| `macos-arm64.ll`, `macos-x86_64.ll` | macOS | made on Linux with the macOS 11.3 SDK; checked as IR only (nobody has run them yet) |
`bootstrap/build.sh` compiles the seed with `clang` (and the runtime, and libclang) into `build/finch0`; `finch0` builds `bootstrap/build.sh` compiles the seed with `clang` (and the runtime, and libclang) into `build/finch0`; `finch0` builds
`src/` into `finch1`; `finch1` builds `src/` again into `build/finch`; the two must make the same IR, or the script stops. `src/` into `finch1`; `finch1` builds `src/` again into `build/finch`; the two must make the same IR, or the script stops.
@ -32,8 +34,12 @@ For another system, from a machine that has that system's headers and a cross co
FINCH_CC=aarch64-linux-gnu-gcc build/finch ir src/main.fch --target arm64 > bootstrap/linux-aarch64.ll FINCH_CC=aarch64-linux-gnu-gcc build/finch ir src/main.fch --target arm64 > bootstrap/linux-aarch64.ll
``` ```
## macOS and Windows ## Windows and macOS
There is no seed for them yet: it needs Apple's or Microsoft's headers, which can't be had on Linux. Until there is one, Windows: `windows-x86_64.ll` is made by `bootstrap/winseed.sh` on Linux (llvm-mingw's MinGW headers and the headers of an
programs for macOS and Windows are built from Linux with `--target macos|windows`, and the compiler's older C++ version official LLVM for Windows); `bootstrap/winbuild.sh` turns it into `finch.exe` for trying under Wine. On a Windows machine
(branch `before-bootstrap`) is the way to get a compiler that runs there. `bootstrap/build.sh` does the same from Git Bash: put llvm-mingw's `bin` in PATH and set `FINCH_LIBCLANG` (the path of
`libclang.dll`), `FINCH_LLVM_INCLUDE` (its `include` folder) and `FINCH_LDFLAGS=-L<folder of libclang.dll>`.
macOS: the seeds were made with the macOS 11.3 SDK; `brew install llvm@21` gives clang and libclang; the CI job sets
`FINCH_CLANG`, `FINCH_LIBCLANG` and `FINCH_LLVM_INCLUDE`. If the macOS build fails, that job is where it shows first.

View file

@ -20,26 +20,32 @@ case "$(uname -s)-$(uname -m)" in
MINGW*|MSYS*|CYGWIN*) seed=windows-x86_64 ;; MINGW*|MSYS*|CYGWIN*) seed=windows-x86_64 ;;
*) echo "bootstrap: no seed for $(uname -s) $(uname -m) (see bootstrap/README.md)" >&2; exit 1 ;; *) echo "bootstrap: no seed for $(uname -s) $(uname -m) (see bootstrap/README.md)" >&2; exit 1 ;;
esac esac
EXE=
case $seed in windows-*) EXE=.exe ;; esac
[ -f "bootstrap/$seed.ll" ] || { echo "bootstrap: bootstrap/$seed.ll is missing" >&2; exit 1; } [ -f "bootstrap/$seed.ll" ] || { echo "bootstrap: bootstrap/$seed.ll is missing" >&2; exit 1; }
CLANG=${FINCH_CLANG:-clang} CLANG=${FINCH_CLANG:-clang}
LIBS=${FINCH_LIBCLANG:--lclang} LIBS=${FINCH_LIBCLANG:--lclang}
INC=
[ -z "${FINCH_LLVM_INCLUDE:-}" ] || INC="-I $FINCH_LLVM_INCLUDE" # where clang-c/Index.h is, if clang does not find it itself
export FINCH_RT=$PWD/runtime/finch_rt.c export FINCH_RT=$PWD/runtime/finch_rt.c
# Windows: libclang.dll has to sit next to the programs that use it (FINCH_LIBCLANG is then the path of that dll)
if [ -n "$EXE" ]; then cp "$LIBS" build/; fi
echo "bootstrap: seed ($seed) -> build/finch0" echo "bootstrap: seed ($seed) -> build/finch0"
$CLANG -O1 -w "bootstrap/$seed.ll" runtime/finch_rt.c -o build/finch0 -lm $LIBS $CLANG -O1 -w "bootstrap/$seed.ll" runtime/finch_rt.c -o build/finch0$EXE -lm $LIBS
echo "bootstrap: build/finch0 packs the runtime and std into src/embedded.fch" echo "bootstrap: build/finch0$EXE packs the runtime and std into src/embedded.fch"
bootstrap/pack.sh build/finch0 bootstrap/pack.sh build/finch0$EXE
echo "bootstrap: build/finch0 builds the compiler -> build/finch1" echo "bootstrap: build/finch0$EXE builds the compiler -> build/finch1"
build/finch0 build src/main.fch -o build/finch1 build/finch0$EXE build src/main.fch -o build/finch1$EXE $INC
echo "bootstrap: build/finch1 builds the compiler -> build/finch" echo "bootstrap: build/finch1$EXE builds the compiler -> build/finch"
build/finch1 build src/main.fch -o build/finch build/finch1$EXE build src/main.fch -o build/finch$EXE $INC
# a compiler that builds itself must come out the same however many times it does # a compiler that builds itself must come out the same however many times it does
build/finch1 ir src/main.fch > build/stage1.ll build/finch1$EXE ir src/main.fch $INC > build/stage1.ll
build/finch ir src/main.fch > build/stage2.ll build/finch$EXE ir src/main.fch $INC > build/stage2.ll
if cmp -s build/stage1.ll build/stage2.ll; then if cmp -s build/stage1.ll build/stage2.ll; then
echo "bootstrap: build/finch1 and build/finch make the same IR ($(wc -l < build/stage2.ll) lines): done" echo "bootstrap: build/finch1$EXE and build/finch make the same IR ($(wc -l < build/stage2.ll) lines): done"
else else
echo "bootstrap: the compiler does not reproduce itself (build/stage1.ll and build/stage2.ll differ)" >&2 echo "bootstrap: the compiler does not reproduce itself (build/stage1.ll and build/stage2.ll differ)" >&2
exit 1 exit 1

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

117504
bootstrap/macos-arm64.ll Normal file

File diff suppressed because one or more lines are too long

117509
bootstrap/macos-x86_64.ll Normal file

File diff suppressed because one or more lines are too long

8
bootstrap/winbuild.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
# Builds build/win/finch.exe from bootstrap/windows-x86_64.ll on Linux (to try the Windows compiler under Wine):
# bootstrap/winbuild.sh <llvm-mingw dir> <LLVM-for-Windows dir>
set -eu
cd "$(dirname "$0")/.."
mkdir -p build/win
"$1/bin/x86_64-w64-mingw32-clang" -O1 -w bootstrap/windows-x86_64.ll runtime/finch_rt.c -o build/win/finch.exe "$2/bin/libclang.dll"
cp "$2/bin/libclang.dll" build/win/

117654
bootstrap/windows-x86_64.ll Normal file

File diff suppressed because one or more lines are too long

9
bootstrap/winseed.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
# Makes bootstrap/windows-x86_64.ll from Linux, with a Linux-hosted llvm-mingw (MinGW headers) and the headers of an
# official LLVM for Windows (clang-c/Index.h):
# bootstrap/winseed.sh <finch> <llvm-mingw dir> <LLVM-for-Windows dir>
set -eu
cd "$(dirname "$0")/.."
finch=$1; mingw=$2; llvm=$3
PATH="$mingw/bin:$PATH" FINCH_CC="$mingw/bin/x86_64-w64-mingw32-clang" "$finch" ir src/main.fch --target windows -I "$llvm/include" > bootstrap/windows-x86_64.ll
echo "wrote bootstrap/windows-x86_64.ll"

View file

@ -534,7 +534,7 @@ fn importHeaders([]Header headers, []str dirs, str target, bool windows, str ccF
index := clang_createIndex(0, 0) index := clang_createIndex(0, 0)
for h in headers { for h in headers {
src := "#include \"" + h.path + "\"\n" src := "#include \"" + h.path + "\"\n"
file := CXUnsavedFile(Filename: "finch_import.c".ptr, Contents: src.ptr, Length: u64(src.len)) file := host.unsavedFile("finch_import.c", src)
ptr tu = null ptr tu = null
flags := CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies flags := CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies
err := clang_parseTranslationUnit2(index, "finch_import.c", argv.ptr, i32(argv.len), addr(file), 1, u32(flags), addr(tu)) err := clang_parseTranslationUnit2(index, "finch_import.c", argv.ptr, i32(argv.len), addr(file), 1, u32(flags), addr(tu))

View file

@ -5773,7 +5773,7 @@ fn runtimeDecls() -> str {
fn assemble(ptr[Gen] g) -> str { fn assemble(ptr[Gen] g) -> str {
text := "; generated by the Finch compiler written in Finch\n\n" text := "; generated by the Finch compiler written in Finch\n\n"
if g.tg.cross { if g.tg.cross || g.tg.windows {
text = text + "target triple = \"" + g.tg.triple + "\"\n\n" text = text + "target triple = \"" + g.tg.triple + "\"\n\n"
} }
text = text + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" text = text + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n"

View file

@ -3,6 +3,12 @@
// the Windows one). // the Windows one).
import "stdlib.h" import "stdlib.h"
import "unistd.h" import "unistd.h"
import "clang-c/Index.h"
// libclang's description of a file that isn't on disk: its Length is C's `unsigned long`, which is 64 bits here
fn unsavedFile(str name, str contents) -> CXUnsavedFile {
return CXUnsavedFile(Filename: name.ptr, Contents: contents.ptr, Length: u64(contents.len))
}
fn quote(str a) -> str { fn quote(str a) -> str {
return "'" + a.replace("'", "'\\''") + "'" return "'" + a.replace("'", "'\\''") + "'"

View file

@ -5,8 +5,14 @@ import "stdio.h"
import "process.h" import "process.h"
import "direct.h" import "direct.h"
import "io.h" import "io.h"
import "clang-c/Index.h"
// a Windows argument: in double quotes, with the quotes inside doubled up the way CreateProcess reads them // a Windows argument: in double quotes, with the quotes inside doubled up the way CreateProcess reads them
// libclang's description of a file that isn't on disk: its Length is C's `unsigned long`, which is 32 bits on Windows
fn unsavedFile(str name, str contents) -> CXUnsavedFile {
return CXUnsavedFile(Filename: name.ptr, Contents: contents.ptr, Length: u32(contents.len))
}
fn quote(str a) -> str { fn quote(str a) -> str {
return "\"" + a.replace("\"", "\\\"") + "\"" return "\"" + a.replace("\"", "\\\"") + "\""
} }
@ -78,7 +84,8 @@ fn makeDirs(str path) {
if part.len == 0 || part.ends_with(":") { if part.len == 0 || part.ends_with(":") {
continue // "" before a leading / and the drive letter continue // "" before a leading / and the drive letter
} }
_mkdir(p.sub(0, at).ptr) // fails when it is there already, which is fine sofar := p.sub(0, at)
_mkdir(sofar) // fails when it is there already, which is fine
} }
} }
@ -93,5 +100,5 @@ fn removeTree(str path) {
// move a file over another // move a file over another
fn move(str from, str to) { fn move(str from, str to) {
delete_file(to) delete_file(to)
rename(from.ptr, to.ptr) rename(from, to)
} }

View file

@ -226,7 +226,7 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
obj := b.out + ".o" obj := b.out + ".o"
write_file(ll, b.ir) write_file(ll, b.ir)
target := "" target := ""
if tg.cross { if tg.cross || tg.windows {
target = " --target=" + tg.triple target = " --target=" + tg.triple
} }
log := b.out + ".log" log := b.out + ".log"
@ -243,6 +243,9 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
if !tg.msvc { if !tg.msvc {
cmd = cmd + " -lm" cmd = cmd + " -lm"
} }
if getenv("FINCH_LDFLAGS").len > 0 {
cmd = cmd + " " + getenv("FINCH_LDFLAGS") // more flags for the linker, like -L/where/libclang/is
}
if b.shared { if b.shared {
if tg.darwin { if tg.darwin {
cmd = cmd + " -dynamiclib" cmd = cmd + " -dynamiclib"

View file

@ -277,8 +277,8 @@ fn main([]str args) -> int {
} }
} }
triple := "" triple := ""
if tg.cross { if tg.cross || tg.windows {
triple = tg.triple triple = tg.triple // on Windows libclang.dll would otherwise assume MSVC, while clang may be MinGW
} }
ci := cimport.importHeaders(headers, dirs, triple, tg.windows, tg.cc) or { ci := cimport.importHeaders(headers, dirs, triple, tg.windows, tg.cc) or {
print(err) print(err)

View file

@ -52,6 +52,9 @@ fn select(str name) -> Target {
t = "x86_64-apple-macosx14.0.0" t = "x86_64-apple-macosx14.0.0"
} }
} }
if t == "x86_64-w64-windows-gnu" {
t = "x86_64-w64-mingw32" // what clang calls the same system when it runs on Windows
}
tg := Target(triple: t) tg := Target(triple: t)
tg.windows = t.contains("windows") || t.contains("mingw") || t.contains("msvc") || t.contains("win32") tg.windows = t.contains("windows") || t.contains("mingw") || t.contains("msvc") || t.contains("win32")
tg.msvc = t.contains("msvc") tg.msvc = t.contains("msvc")
@ -59,7 +62,8 @@ fn select(str name) -> Target {
tg.arm64 = t.starts_with("aarch64") || t.starts_with("arm64") tg.arm64 = t.starts_with("aarch64") || t.starts_with("arm64")
hostArm := hostName.starts_with("aarch64") || hostName.starts_with("arm64") hostArm := hostName.starts_with("aarch64") || hostName.starts_with("arm64")
hostDarwin := hostName.contains("apple") || hostName.contains("darwin") hostDarwin := hostName.contains("apple") || hostName.contains("darwin")
tg.cross = tg.windows || tg.darwin != hostDarwin || tg.arm64 != hostArm hostWin := hostName.contains("windows") || hostName.contains("mingw") || hostName.contains("msvc")
tg.cross = tg.windows != hostWin || tg.darwin != hostDarwin || tg.arm64 != hostArm
tg.exe = "" tg.exe = ""
if tg.windows { if tg.windows {
tg.exe = ".exe" tg.exe = ".exe"