Finch/boot/main.fch
Nexoniarz 8c3b5b15b9 -g: debug info, in the compiler written in Finch
DWARF as LLVM metadata: a compile unit, a subprogram per function, a location on every call, a variable
for each local and parameter with types a debugger can open (structs with their fields, arrays and text
as ptr/len/cap, maps, enums with their variants, pointers, fixed arrays). Methods show self.

Use it with -O0, as the docs say: the optimizer inlines the program into main, which has no debug info.
boot.sh builds ten programs with -g -O0 and checks they still behave. 51 passed.
2026-10-25 21:08:13 +01:00

276 lines
8.1 KiB
Text

// finch-boot: the Finch compiler written in Finch.
//
// finch-boot build <file.fch> [-o name] compile a program
// finch-boot ir <file.fch> print the LLVM IR
//
// It reads the program and the modules it imports, checks it, writes LLVM IR as text
// and lets clang turn that into a program, linked with Finch's C runtime.
import lexer
import ast
import parser
import gen
import cimport
import "stdlib.h"
fn dirOf(str path) -> str {
i := path.len - 1
while i >= 0 && path[i] != '/' {
i -= 1
}
if i < 0 {
return "."
}
return path.sub(0, i)
}
fn stem(str path) -> str {
name := path.sub(dirOf(path).len + 1, path.len)
if dirOf(path) == "." && !path.starts_with("./") {
name = path
}
dot := name.find(".")
if dot > 0 {
return name.sub(0, dot)
}
return name
}
struct Loader {
[]ast.Program progs
[]str files
[]str loaded
str std // the standard library's folder, or ""
[]str paths // FINCH_PATH: more folders to look for modules in
}
// import json: the file next to the one that imports it, else the standard library's.
// A program's own json.fch wins over the library's.
fn findModule(ptr[Loader] l, str from, str name) -> str {
local := dirOf(from) + "/" + name + ".fch"
if file_exists(local) {
return local
}
for d in l.paths {
if file_exists(d + "/" + name + ".fch") {
return d + "/" + name + ".fch"
}
}
shared := l.std + "/" + name + ".fch"
if file_exists(shared) {
return shared
}
return local
}
fn load(ptr[Loader] l, str path, str module) {
if !file_exists(path) {
if module.len > 0 {
print("error: can't find the module '" + module + "' (looked for " + path + ")")
} else {
print("error: can't open '" + path + "'")
}
exit(1)
}
src := read_file(path)
file := l.files.len
l.files.push(path)
prog := parser.parse(lexer.lex(src, path), file, path)
prog.module = module
prog.path = path
imports := prog.imports
l.progs.push(prog)
for name in imports {
if !l.loaded.contains(name) {
l.loaded.push(name)
load(l, findModule(l, path, name), name)
}
}
}
// -lname, or whatever pkg-config says the library needs
fn libFlags(str name) -> str {
tmp := "/tmp/finch_pkg_flags.txt"
code := shell("pkg-config --libs '" + name + "' > '" + tmp + "' 2> /dev/null")
if code == 0 && file_exists(tmp) {
flags := read_file(tmp).trim()
shell("rm -f '" + tmp + "'")
if flags.len > 0 {
return flags
}
}
return "-l" + name
}
fn usage() {
print("Finch -- the compiler is written in Finch")
print("usage:")
print(" finch run <file.fch> [args...] build and run a program")
print(" finch build <file.fch> [-o name] build a program")
print(" finch ir <file.fch> print the LLVM IR")
print(" finch version show the version")
print("options:")
print(" -O0 no optimization (faster to build)")
print(" -l <lib> link a library (the same as link \"lib\" in the program)")
print(" -I <dir> look for imported C headers there too")
print("not in this compiler yet: -g, --target, lsp. They are in the C++ one (branch before-bootstrap).")
exit(1)
}
fn quote(str a) -> str {
return "'" + a.replace("'", "'\\''") + "'"
}
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)")
return 0
}
if args.len < 3 {
usage()
}
cmd := args[1]
file := args[2]
if cmd != "build" && cmd != "run" && cmd != "ir" {
usage()
}
out := stem(file)
outGiven := false
shared := false
debug := false
opt := "-O2"
[]str extraLibs
[]str extraDirs
[]str progArgs
ai := 3
while ai < args.len {
a := args[ai]
if cmd == "run" && progArgs.len > 0 { // after the first argument of the program, all of them are its
progArgs.push(a)
} else if a == "-o" && ai + 1 < args.len && cmd == "build" {
out = args[ai + 1]
outGiven = true
ai += 1
} else if a == "--shared" && cmd == "build" {
shared = true
} else if a == "-O0" {
opt = "-O0"
} else if a == "-l" && ai + 1 < args.len {
extraLibs.push(libFlags(args[ai + 1]))
ai += 1
} else if a.starts_with("-l") && a.len > 2 {
extraLibs.push(libFlags(a.sub(2, a.len)))
} else if a == "-I" && ai + 1 < args.len {
extraDirs.push(args[ai + 1])
ai += 1
} else if a == "-g" {
debug = true
} else if a == "--target" {
print("error: '--target' is not in the compiler written in Finch yet; the C++ one has it (branch before-bootstrap)")
return 1
} else if cmd == "run" {
progArgs.push(a)
} else {
usage()
}
ai += 1
}
if cmd == "run" {
out = "/tmp/finch_run_" + stem(file)
}
if shared && !outGiven {
out = "lib" + out + ".so"
}
loader := Loader()
for cand in [dirOf(args[0]) + "/../std", dirOf(args[0]) + "/std", "std"] {
if loader.std.len == 0 && file_exists(cand + "/path.fch") {
loader.std = cand
}
}
for d in getenv("FINCH_PATH").split(":") {
if d.len > 0 {
loader.paths.push(d)
}
}
load(addr(loader), file, "")
// C headers, from every file; their folders are searched first for "local.h"
[]cimport.Header headers
[]str seen
[]str dirs = extraDirs
[]str libs = extraLibs
for prog in loader.progs {
d := dirOf(prog.path)
if !dirs.contains(d) {
dirs.push(d)
}
for i in 0..prog.cimports.len {
if !seen.contains(prog.cimports[i]) {
seen.push(prog.cimports[i])
headers.push(cimport.Header(path: prog.cimports[i], where: prog.cwhere[i]))
}
}
for l in prog.links {
if l.ends_with(".c") {
libs.push("'" + d + "/" + l + "'")
} else {
libs.push(libFlags(l))
}
}
}
ci := cimport.importHeaders(headers, dirs) or {
print(err)
return 1
}
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, cwd: getenv("PWD")))
if cmd == "ir" {
print(ir)
return 0
}
// the runtime is Finch's C file; FINCH_RT can point somewhere else
rt := getenv("FINCH_RT")
if rt.len == 0 {
rt = dirOf(args[0]) + "/../runtime/finch_rt.c"
}
if !file_exists(rt) {
rt = dirOf(args[0]) + "/runtime/finch_rt.c"
}
if !file_exists(rt) && file_exists("runtime/finch_rt.c") {
rt = "runtime/finch_rt.c"
}
if !file_exists(rt) {
print("error: can't find the Finch runtime (runtime/finch_rt.c)")
return 1
}
llPath := out + ".ll"
write_file(llPath, ir)
sharedFlags := ""
if shared {
sharedFlags = " -shared -fPIC"
}
if debug {
sharedFlags = sharedFlags + " -g" // or clang drops the debug info that is in the IR
}
// the compiler of the IR is always a clang (CC may be gcc, which can't read it); FINCH_CC picks another clang
cc := getenv("FINCH_CC")
if cc.len == 0 {
cc = "clang"
}
code := shell(cc + " " + opt + sharedFlags + " -w '" + llPath + "' '" + rt + "' -o '" + out + "' -lm " + libs.join(" "))
shell("rm -f '" + llPath + "'")
if code != 0 {
print("error: clang failed")
return 1
}
if cmd == "run" {
[]str quoted
for a in progArgs {
quoted.push(quote(a))
}
status := shell(quote(out) + " " + quoted.join(" "))
shell("rm -f " + quote(out))
return status
}
return 0
}