The compiler carries its runtime and std inside itself; host module for the system it runs on

src/embedded.fch (made by tools/pack.fch, checked by tests/run.sh) holds runtime/finch_rt.c and std/; when they aren't
beside the binary they're written to the cache folder once. All the sh/cmd specifics of the driver (quoting, temp dir,
mkdir, rm, mv, null device, path separator, cache dir, binary stdio) moved into host.fch / host_windows.fch.

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-25 22:32:02 +01:00
commit 837ad45177
12 changed files with 354 additions and 59 deletions

View file

@ -6,8 +6,8 @@
# bootstrap/build.sh seed also write bootstrap/<system>.ll anew (do this when the compiler got a feature
# its own source uses, or the seed is old: the next build starts from it)
#
# Needs clang and libclang (the C headers are read through it). The compiler is then in build/finch; it looks for the
# runtime and the standard library next to build/, in runtime/ and std/.
# Needs clang and libclang (the C headers are read through it). The compiler is then in build/finch. It carries the
# runtime and the standard library inside itself (src/embedded.fch), so it runs from anywhere.
set -eu
cd "$(dirname "$0")/.."
mkdir -p build
@ -28,6 +28,8 @@ export FINCH_RT=$PWD/runtime/finch_rt.c
echo "bootstrap: seed ($seed) -> build/finch0"
$CLANG -O1 -w "bootstrap/$seed.ll" runtime/finch_rt.c -o build/finch0 -lm $LIBS
echo "bootstrap: build/finch0 packs the runtime and std into src/embedded.fch"
bootstrap/pack.sh build/finch0
echo "bootstrap: build/finch0 builds the compiler -> build/finch1"
build/finch0 build src/main.fch -o build/finch1
echo "bootstrap: build/finch1 builds the compiler -> build/finch"

6
bootstrap/pack.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
# Writes src/embedded.fch: the runtime and the standard library as text, so the compiler can carry them inside itself.
# bootstrap/pack.sh <a Finch compiler> [out.fch]
set -eu
cd "$(dirname "$0")/.."
FINCH_RT=$PWD/runtime/finch_rt.c "$1" run tools/pack.fch "${2:-src/embedded.fch}" runtime/finch_rt.c std/*.fch std/*.h

View file

@ -8,6 +8,7 @@
//
// C structs by value appear in types as the named type `module "C"`.
import ast
import host
import "clang-c/Index.h"
link "clang"
@ -101,8 +102,11 @@ fn fixed(ast.Type elem, int count) -> ast.Type {
// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would.
fn systemIncludeArgs(str cc) -> []str {
[]str dirs
tmp := "/tmp/finch_cc_dirs.txt"
shell(cc + " -E -v -x c /dev/null 2> '" + tmp + "' > /dev/null")
tmp := host.tempDir() + "/finch_cc_dirs_" + str(host.pid()) + ".txt"
empty := host.tempDir() + "/finch_empty_" + str(host.pid()) + ".c"
write_file(empty, "")
host.run(cc + " -E -v -x c " + host.quote(empty) + " 2> " + host.quote(tmp) + " > " + host.nullDevice())
host.remove(empty)
if !file_exists(tmp) {
return dirs
}
@ -126,7 +130,7 @@ fn systemIncludeArgs(str cc) -> []str {
dirs.push("-isystem" + l)
}
}
shell("rm -f '" + tmp + "'")
host.remove(tmp)
return dirs
}

28
src/embedded.fch Normal file

File diff suppressed because one or more lines are too long

77
src/host.fch Normal file
View file

@ -0,0 +1,77 @@
// What differs between the systems the compiler itself runs on: how a command line is quoted and run,
// where temporary files go, making and removing files. This one is for Linux and macOS (name_windows.fch has
// the Windows one).
import "stdlib.h"
import "unistd.h"
fn quote(str a) -> str {
return "'" + a.replace("'", "'\\''") + "'"
}
// run a command line through the shell; its exit code
fn run(str cmd) -> int {
return shell(cmd)
}
fn nullDevice() -> str {
return "/dev/null"
}
// the folder for temporary files
fn tempDir() -> str {
t := getenv("TMPDIR")
if t.len > 0 {
if t.ends_with("/") {
return t.sub(0, t.len - 1)
}
return t
}
return "/tmp"
}
fn pathSeparator() -> str {
return ":"
}
fn pid() -> int {
return int(getpid())
}
// the folder the compiler was started in
fn cwd() -> str {
return getenv("PWD")
}
// where files that are made once and kept go
fn cacheDir() -> str {
x := getenv("XDG_CACHE_HOME")
if x.len > 0 {
return x + "/finch"
}
h := getenv("HOME")
if h.len > 0 {
return h + "/.cache/finch"
}
return tempDir() + "/finch-cache"
}
// the editor protocol is bytes, not text lines
fn binaryStdio() {
}
fn makeDirs(str path) {
shell("mkdir -p " + quote(path))
}
fn remove(str path) {
delete_file(path)
}
fn removeTree(str path) {
shell("rm -rf " + quote(path))
}
// move a file over another
fn move(str from, str to) {
shell("mv " + quote(from) + " " + quote(to))
}

97
src/host_windows.fch Normal file
View file

@ -0,0 +1,97 @@
// What differs between the systems the compiler itself runs on: how a command line is quoted and run,
// where temporary files go, making and removing files. This one is for Windows (host.fch is for Linux and macOS).
import "stdlib.h"
import "stdio.h"
import "process.h"
import "direct.h"
import "io.h"
// a Windows argument: in double quotes, with the quotes inside doubled up the way CreateProcess reads them
fn quote(str a) -> str {
return "\"" + a.replace("\"", "\\\"") + "\""
}
// Paths use / inside the compiler; cmd.exe wants \
fn native(str path) -> str {
return path.replace("/", "\\")
}
// run a command line through cmd.exe; its exit code.
// system() runs `cmd /c <line>`, and cmd drops the first and last quote of a line that starts with one: so the line
// gets one more pair around it.
fn run(str cmd) -> int {
return shell("\"" + cmd + "\"")
}
fn nullDevice() -> str {
return "NUL"
}
fn tempDir() -> str {
for name in ["TEMP", "TMP"] {
t := getenv(name)
if t.len > 0 {
return t.replace("\\", "/")
}
}
return "."
}
fn pathSeparator() -> str {
return ";"
}
fn pid() -> int {
return int(getpid())
}
fn cwd() -> str {
here := _getcwd(null, 0) // C allocates the buffer
if here.len == 0 {
return "."
}
return here.replace("\\", "/")
}
fn cacheDir() -> str {
x := getenv("LOCALAPPDATA")
if x.len > 0 {
return x.replace("\\", "/") + "/finch"
}
return tempDir() + "/finch-cache"
}
// the editor protocol is bytes, not text lines: no \r\n translation on stdin and stdout
fn binaryStdio() {
_setmode(0, 32768) // _O_BINARY
_setmode(1, 32768)
}
fn makeDirs(str path) {
p := path.replace("\\", "/")
at := 0
for part in p.split("/") {
if at > 0 {
at += 1
}
at += part.len
if part.len == 0 || part.ends_with(":") {
continue // "" before a leading / and the drive letter
}
_mkdir(p.sub(0, at).ptr) // fails when it is there already, which is fine
}
}
fn remove(str path) {
delete_file(path)
}
fn removeTree(str path) {
shell("\"rmdir /s /q " + quote(native(path)) + " 2> NUL\"")
}
// move a file over another
fn move(str from, str to) {
delete_file(to)
rename(from.ptr, to.ptr)
}

View file

@ -2,8 +2,7 @@
// and C files the program asks for, and turn the linker's complaints into something a person can act on.
import platform
import cimport
import "stdlib.h"
import "unistd.h"
import host
// 32-bit FNV-1a, as hex: names the cached runtime object after what it was built from
fn fingerprint(str text) -> str {
@ -20,20 +19,7 @@ fn fingerprint(str text) -> str {
}
fn quote(str a) -> str {
return "'" + a.replace("'", "'\\''") + "'"
}
// where files that are made once and kept go: ~/.cache/finch
fn cacheDir() -> str {
x := getenv("XDG_CACHE_HOME")
if x.len > 0 {
return x + "/finch"
}
h := getenv("HOME")
if h.len > 0 {
return h + "/.cache/finch"
}
return "/tmp/finch-cache"
return host.quote(a)
}
// "-lname", or whatever pkg-config says the library needs
@ -42,11 +28,11 @@ fn libFlags(str name, platform.Target tg) -> str {
return "-framework " + quote(name.sub(0, name.len - 10)) // macOS: link "Cocoa.framework"
}
if !tg.cross {
tmp := "/tmp/finch_pkg_flags.txt"
code := shell("pkg-config --libs " + quote(name) + " > '" + tmp + "' 2> /dev/null")
tmp := host.tempDir() + "/finch_pkg_flags_" + str(host.pid()) + ".txt"
code := host.run("pkg-config --libs " + quote(name) + " > " + quote(tmp) + " 2> " + host.nullDevice())
if code == 0 && file_exists(tmp) {
flags := (read_file(tmp) or "").trim()
shell("rm -f '" + tmp + "'")
host.remove(tmp)
if flags.len > 0 {
return flags
}
@ -185,25 +171,27 @@ fn linkFailed(str output, cimport.CImports ci, platform.Target tg) -> int {
// The runtime, compiled once per system and compiler and kept in the cache folder.
fn runtimeObject(str rt, platform.Target tg, str cc) -> str! {
source := try read_file(rt)
dir := cacheDir()
dir := host.cacheDir()
obj := dir + "/rt-" + fingerprint(source + cc + tg.triple + "2.5") + ".o"
if file_exists(obj) {
return obj
}
shell("mkdir -p " + quote(dir))
part := obj + ".part" + str(int(getpid()))
host.makeDirs(dir)
part := obj + ".part" + str(host.pid())
pic := " -fPIC"
if tg.windows {
pic = ""
}
log := part + ".log"
code := shell(cc + " -O2" + pic + " -c " + quote(rt) + " -o " + quote(part) + " > " + quote(log) + " 2>&1")
code := host.run(cc + " -O2" + pic + " -c " + quote(rt) + " -o " + quote(part) + " > " + quote(log) + " 2>&1")
if code != 0 {
text := read_file(log) or ""
shell("rm -f " + quote(part) + " " + quote(log))
host.remove(part)
host.remove(log)
return error(text + "error: couldn't build the Finch runtime with '" + cc + "' (is a C compiler installed?)")
}
shell("rm -f " + quote(log) + " && mv " + quote(part) + " " + quote(obj))
host.remove(log)
host.move(part, obj)
return obj
}
@ -242,12 +230,13 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
target = " --target=" + tg.triple
}
log := b.out + ".log"
code := shell(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1")
shell("rm -f " + quote(ll))
code := host.run(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1")
host.remove(ll)
if code != 0 {
print(read_file(log) or "")
print("error: clang couldn't compile the program's IR")
shell("rm -f " + quote(obj) + " " + quote(log))
host.remove(obj)
host.remove(log)
return 1
}
cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
@ -270,10 +259,12 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
if tg.windows {
pic = ""
}
c := shell(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1")
c := host.run(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1")
if c != 0 {
print((read_file(log) or "") + "error: couldn't compile " + l)
shell("rm -f " + quote(obj) + " " + quote(log) + " " + quote(o))
host.remove(obj)
host.remove(log)
host.remove(o)
return 1
}
temps.push(o)
@ -284,11 +275,12 @@ fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
cmd = cmd + " " + libFlags(l, tg)
}
}
status := shell(cmd + " > " + quote(log) + " 2>&1")
status := host.run(cmd + " > " + quote(log) + " 2>&1")
output := read_file(log) or ""
shell("rm -f " + quote(obj) + " " + quote(log))
host.remove(obj)
host.remove(log)
for t in temps {
shell("rm -f " + quote(t))
host.remove(t)
}
if status == 127 {
print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)")

View file

@ -5,9 +5,9 @@
// It asks the compiler instead of being one: for every change it runs `finch index` on the text the editor has
// (see the index records in gen.fch), and answers from what comes back.
import json
import host
import docs
import "stdio.h"
import "unistd.h"
import "stdlib.h"
// ---------- what one run of the compiler told ----------
@ -325,17 +325,17 @@ fn parseOutput(str output) -> Analysis {
fn Server.analyse(str path) {
// the editor's text, as files the compiler can read: it asks for the file by its real name
shell("mkdir -p '" + self.tmp + "'")
host.makeDirs(self.tmp)
overlays := ""
n := 0
for p in self.docs.keys() {
copy := self.tmp + "/doc" + str(n) + ".fch"
write_file(copy, self.docs[p])
overlays = overlays + " --overlay '" + p + "=" + copy + "'"
overlays = overlays + " --overlay " + host.quote(p + "=" + copy)
n += 1
}
out := self.tmp + "/out.txt"
shell("'" + self.self + "' index '" + path + "'" + overlays + " > '" + out + "' 2>&1")
host.run(host.quote(self.self) + " index " + host.quote(path) + overlays + " > " + host.quote(out) + " 2>&1")
a := parseOutput(read_file(out) or "")
if !a.diag.has {
self.good[path] = a
@ -863,13 +863,14 @@ fn Server.answer(json.Json msg) {
// finch lsp
fn run(str self) -> int {
s := Server(self: self, tmp: "/tmp/finch_lsp_" + str(int(getpid())))
host.binaryStdio()
s := Server(self: self, tmp: host.tempDir() + "/finch_lsp_" + str(host.pid()))
while true {
msg := readMessage() or {
break
}
s.answer(msg)
}
shell("rm -rf '" + s.tmp + "'")
host.removeTree(s.tmp)
return 0
}

View file

@ -13,6 +13,8 @@ import cimport
import platform
import linker
import lsp
import host
import embedded
import "stdlib.h"
fn dirOf(str path) -> str {
@ -129,6 +131,23 @@ fn usage() {
exit(1)
}
// The runtime and the standard library are carried inside the compiler (embedded.fch). When they aren't found as
// files next to it, they are written to the cache folder, once, and used from there.
fn unpack() -> str {
names := embedded.names()
dir := host.cacheDir() + "/files-" + linker.fingerprint(embedded.texts().join("\n"))
if file_exists(dir + "/done") {
return dir
}
texts := embedded.texts()
for i in 0..names.len {
host.makeDirs(dirOf(dir + "/" + names[i]))
write_file(dir + "/" + names[i], texts[i])
}
write_file(dir + "/done", "")
return dir
}
fn main([]str args) -> int {
if args.len == 2 && (args[1] == "version" || args[1] == "--version") {
print("Finch 2.4.0 (the compiler is written in Finch)")
@ -196,7 +215,7 @@ fn main([]str args) -> int {
}
tg := platform.select(targetName)
if cmd == "run" {
out = "/tmp/finch_run_" + stem(file) + tg.exe
out = host.tempDir() + "/finch_run_" + str(host.pid()) + "_" + stem(file) + tg.exe
} else if shared && !outGiven {
if tg.windows {
out = out + ".dll"
@ -221,9 +240,12 @@ fn main([]str args) -> int {
loader.std = cand
}
}
if loader.std.len == 0 {
loader.std = unpack() + "/std"
}
loader.overlayPaths = overlayPaths
loader.overlayFiles = overlayFiles
for d in getenv("FINCH_PATH").split(":") {
for d in getenv("FINCH_PATH").split(host.pathSeparator()) {
if d.len > 0 {
loader.paths.push(d)
}
@ -262,7 +284,7 @@ fn main([]str args) -> int {
print(err)
return 1
}
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: getenv("PWD"), target: tg))
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: host.cwd(), target: tg))
if cmd == "ir" || cmd == "index" {
print(ir)
return 0
@ -283,8 +305,7 @@ fn main([]str args) -> int {
rt = "runtime/finch_rt.c"
}
if !file_exists(rt) {
print("error: can't find the Finch runtime (runtime/finch_rt.c)")
return 1
rt = unpack() + "/runtime/finch_rt.c"
}
extra := ""
if shared {
@ -301,8 +322,8 @@ fn main([]str args) -> int {
for a in progArgs {
quoted.push(linker.quote(a))
}
status := shell(tg.runner + linker.quote(out) + " " + quoted.join(" "))
shell("rm -f " + linker.quote(out))
status := host.run(tg.runner + linker.quote(out) + " " + quoted.join(" "))
host.remove(out)
return status
}
return 0

View file

@ -1,6 +1,7 @@
// The system a program is built for, and what depends on it: the C compiler that links, the file
// name of the program, how it is run here, and (through the flags) the C calling convention.
// The same as src/target.cpp.
import host
import "stdlib.h"
struct Target {
@ -22,10 +23,10 @@ fn firstLine(str text) -> str {
// what clang calls the system it runs on, like x86_64-pc-linux-gnu
fn hostTriple() -> str {
tmp := "/tmp/finch_triple.txt"
shell("clang -dumpmachine > '" + tmp + "' 2> /dev/null")
tmp := host.tempDir() + "/finch_triple_" + str(host.pid()) + ".txt"
host.run("clang -dumpmachine > " + host.quote(tmp) + " 2> " + host.nullDevice())
out := read_file(tmp) or ""
shell("rm -f '" + tmp + "'")
host.remove(tmp)
if out.trim().len == 0 {
return "x86_64-unknown-linux-gnu"
}
@ -34,10 +35,10 @@ fn hostTriple() -> str {
// "windows", "linux", "macos", "arm64" (ARM64 Linux), or a full LLVM triple; "" = this system
fn select(str name) -> Target {
host := hostTriple()
hostName := hostTriple()
t := name
if name.len == 0 {
t = host
t = hostName
} else if name == "windows" {
t = "x86_64-w64-mingw32"
} else if name == "linux" {
@ -45,7 +46,7 @@ fn select(str name) -> Target {
} else if name == "arm64" || name == "aarch64" || name == "linux-arm64" {
t = "aarch64-unknown-linux-gnu"
} else if name == "macos" {
if host.starts_with("aarch64") || host.starts_with("arm64") {
if hostName.starts_with("aarch64") || hostName.starts_with("arm64") {
t = "arm64-apple-macosx14.0.0"
} else {
t = "x86_64-apple-macosx14.0.0"
@ -56,8 +57,8 @@ fn select(str name) -> Target {
tg.msvc = t.contains("msvc")
tg.darwin = t.contains("apple") || t.contains("darwin") || t.contains("macos")
tg.arm64 = t.starts_with("aarch64") || t.starts_with("arm64")
hostArm := host.starts_with("aarch64") || host.starts_with("arm64")
hostDarwin := host.contains("apple") || host.contains("darwin")
hostArm := hostName.starts_with("aarch64") || hostName.starts_with("arm64")
hostDarwin := hostName.contains("apple") || hostName.contains("darwin")
tg.cross = tg.windows || tg.darwin != hostDarwin || tg.arm64 != hostArm
tg.exe = ""
if tg.windows {
@ -73,6 +74,8 @@ fn select(str name) -> Target {
tg.cc = "aarch64-linux-gnu-gcc"
} else if tg.cross {
tg.cc = "clang --target=" + t
} else if host.pathSeparator() == ";" {
tg.cc = "clang" // Windows has no cc
} else {
tg.cc = "cc"
}

View file

@ -10,6 +10,7 @@ set -u
cd "$(dirname "$0")"
FINCH=${FINCH:-../build/finch}
FINCH="$(cd "$(dirname "$FINCH")" && pwd)/$(basename "$FINCH")" # tests/shared.sh runs from another folder
FINCH_ROOT="$(cd .. && pwd)"
export FINCH_RT=${FINCH_RT:-$PWD/../runtime/finch_rt.c}
pass=0; failed=0
tmp=$(mktemp -d)
@ -78,6 +79,15 @@ else
head -5 "$tmp/shared.out"
fi
# the runtime and std carried inside the compiler are the files as they are now
"$FINCH_ROOT/bootstrap/pack.sh" "$FINCH" "$tmp/embedded.fch" 2>/dev/null
if cmp -s "$tmp/embedded.fch" "$FINCH_ROOT/src/embedded.fch"; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL src/embedded.fch is out of date (run bootstrap/build.sh)"
fi
# the language server, talked to the way an editor does
if command -v python3 > /dev/null 2>&1; then
if FINCH="$FINCH" python3 lsp_test.py > "$tmp/lsp.out" 2>&1; then

54
tools/pack.fch Normal file
View file

@ -0,0 +1,54 @@
// finch run tools/pack.fch src/embedded.fch runtime/finch_rt.c std/json.fch ...
// Writes a Finch module that holds the given files as text, so the compiler can carry its runtime and standard library
// inside itself (bootstrap/build.sh runs this; tests/run.sh checks the result is up to date).
fn literal(str text) -> str {
[]str parts = ["\""]
for c in text {
if c == '\\' {
parts.push("\\\\")
} else if c == '"' {
parts.push("\\\"")
} else if c == '\n' {
parts.push("\\n")
} else if c == '\t' {
parts.push("\\t")
} else if c == '\r' {
parts.push("\\r")
} else {
parts.push(str(c))
}
}
parts.push("\"")
return parts.join("")
}
fn main([]str args) -> int {
if args.len < 3 {
print("usage: pack out.fch file...")
return 1
}
[]str out = [
"// The runtime and the standard library, as text. Made by tools/pack.fch: don't edit it, run bootstrap/build.sh.",
"// Used when the compiler can't find runtime/ and std/ next to itself.",
"",
"fn names() -> []str {",
" []str list",
]
for i in 2..args.len {
out.push(" list.push(" + literal(args[i]) + ")")
}
out.push(" return list")
out.push("}")
out.push("")
out.push("fn texts() -> []str {")
out.push(" []str list")
for i in 2..args.len {
out.push(" list.push(" + literal(read_file(args[i])) + ")")
}
out.push(" return list")
out.push("}")
out.push("")
write_file(args[1], out.join("\n"))
return 0
}