Structs, arrays, owned strings, memory model, modules, debug info

- struct with default values; constructors Point(1, 2) / Point(x: 1); field access, auto-deref through pointers
- []T arrays with bounds checks, literals, for-each, push/pop/insert/remove/clear/resize/find/contains/join/slice/reverse/sort
- str carries its length: .len, s[i], +, comparison, sub/find/split/trim/upper/lower/replace/repeat/bytes, str(x), int(s), float(s)
- Memory: values own their memory and are freed at the end of their block; assignment copies, temporaries move;
  parameters are lent and only copied when the function changes them; defer; new()/free() for manual heap memory
- input(), read_file/write_file/file_exists, exit(), fn main([]str args)
- Finch modules: import name, module.fn(), module.Struct
- C structs from headers (fields, fixed arrays, nested); passed and returned by value with the System V x86-64 ABI;
  addr(fn) gives C a callback
- link "file.c" compiles and links your own C code
- -g emits DWARF debug info (gdb/lldb)
- Small C runtime (runtime/finch_rt.c) embedded in the compiler, cached in ~/.cache/finch
- Tests: 58, all clean under valgrind (MEMCHECK=1 tests/run.sh)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-08 18:57:37 +02:00
commit 1dc976bd8f
59 changed files with 4577 additions and 1047 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(finch VERSION 1.0.0 LANGUAGES CXX) project(finch VERSION 2.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -13,9 +13,19 @@ add_executable(finch
src/lexer.cpp src/lexer.cpp
src/parser.cpp src/parser.cpp
src/codegen.cpp src/codegen.cpp
src/codegen_types.cpp
src/codegen_builtins.cpp
src/abi.cpp
src/cimport.cpp src/cimport.cpp
) )
# The runtime (runtime/finch_rt.c) is embedded into the compiler as a string;
# finch compiles it once into ~/.cache/finch and links it into every program.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c)
file(READ ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c FINCH_RT_SOURCE)
file(WRITE ${CMAKE_BINARY_DIR}/rt_source.inc "static const char *kRuntimeSource = R\"FINCHRT(${FINCH_RT_SOURCE})FINCHRT\";\n")
target_include_directories(finch PRIVATE ${CMAKE_BINARY_DIR})
# libclang reads C headers for `import "stdio.h"` # libclang reads C headers for `import "stdio.h"`
find_path(LIBCLANG_INCLUDE clang-c/Index.h HINTS ${LLVM_INCLUDE_DIRS}) find_path(LIBCLANG_INCLUDE clang-c/Index.h HINTS ${LLVM_INCLUDE_DIRS})
find_library(LIBCLANG NAMES clang libclang HINTS ${LLVM_LIBRARY_DIRS}) find_library(LIBCLANG NAMES clang libclang HINTS ${LLVM_LIBRARY_DIRS})

27
examples/guess.fn Normal file
View file

@ -0,0 +1,27 @@
// Guess the number: input, loops and conversions.
import "stdlib.h"
import "time.h"
fn main() {
srand(u32(time(null)))
secret := rand() % 100 + 1
tries := 0
print("I'm thinking of a number from 1 to 100.")
while true {
answer := input("Your guess: ")
if answer.len == 0 {
print("Bye!")
return
}
guess := int(answer)
tries += 1
if guess < secret {
print("Higher!")
} else if guess > secret {
print("Lower!")
} else {
print("Yes! It was", secret, "- you needed", tries, "tries.")
return
}
}
}

49
examples/structs.fn Normal file
View file

@ -0,0 +1,49 @@
// Structs, arrays of structs, and a linked list on the heap.
import "math.h"
struct Point {
float x
float y
}
struct Shape {
str name
[]Point corners
int color = 0xFF8800
}
struct Node {
int value
ptr[Node] next
}
fn perimeter(Shape s) -> float {
total := 0.0
for i in 0..s.corners.len {
a := s.corners[i]
b := s.corners[(i + 1) % s.corners.len]
dx := b.x - a.x
dy := b.y - a.y
total += sqrt(dx * dx + dy * dy)
}
return total
}
fn main() {
square := Shape(name: "square", corners: [Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)])
print(square.name, square.corners.len, "corners, perimeter", perimeter(square))
print(square)
// a linked list: new(...) puts a value on the heap, free(...) gives it back
ptr[Node] head = null
for i in 0..3 {
head = new(Node(value: i, next: head))
}
n := head
while n != null {
print("node", n.value)
next := n.next
free(n)
n = next
}
}

53
examples/todo.fn Normal file
View file

@ -0,0 +1,53 @@
// A tiny to-do list: structs, arrays, strings and files.
struct Task {
str text
bool done = false
}
fn show([]Task tasks) {
if tasks.len == 0 {
print("(nothing to do)")
}
for i in 0..tasks.len {
mark := " "
if tasks[i].done {
mark = "x"
}
print("[" + mark + "]", i + 1, tasks[i].text)
}
}
fn save([]Task tasks, str path) {
[]str lines
for t in tasks {
lines.push(str(t.done) + "|" + t.text)
}
write_file(path, lines.join("\n"))
}
fn load(str path) -> []Task {
[]Task tasks
if !file_exists(path) {
return tasks
}
for line in read_file(path).split("\n") {
if line.len == 0 {
continue
}
parts := line.split("|")
tasks.push(Task(text: parts[1], done: parts[0] == "true"))
}
return tasks
}
fn main() {
path := "todo.txt"
tasks := load(path)
tasks.push(Task(text: "learn Finch"))
tasks.push(Task(text: "write a game"))
tasks[0].done = true
show(tasks)
save(tasks, path)
print("saved", tasks.len, "tasks to", path)
}

426
runtime/finch_rt.c Normal file
View file

@ -0,0 +1,426 @@
// The Finch runtime: string and array helpers, input, files and runtime errors.
// It is embedded in the compiler, compiled once into a cache folder, and linked
// statically into every Finch program. Everything here is plain C with no state.
//
// Layout shared with the compiler (src/codegen.cpp):
// str = { char *ptr; int64 len; int64 cap } ptr is always NUL-terminated
// cap > 0 : heap memory owned by the value (cap bytes allocated)
// cap == 0 : a string literal, valid forever, never written to
// cap == -1: borrowed from C, must be copied before it is kept
// []T = { T *ptr; int64 len; int64 cap } owns ptr when cap > 0
#include <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *ptr;
int64_t len;
int64_t cap;
} FStr;
typedef struct {
void *ptr;
int64_t len;
int64_t cap;
} FArr;
static char empty[1] = "";
// ---------- runtime errors ----------
__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) {
fflush(stdout);
fprintf(stderr, "%s:%lld: runtime error: %s\n", file, (long long)line, msg);
exit(1);
}
__attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) {
char msg[128];
snprintf(msg, sizeof msg, "index %lld is out of range (the length is %lld)", (long long)i, (long long)len);
finch_panic(file, line, msg);
}
static void *must(void *p) {
if (!p) {
fflush(stdout);
fprintf(stderr, "runtime error: out of memory\n");
exit(1);
}
return p;
}
void *finch_alloc(int64_t n) { return must(malloc(n ? (size_t)n : 1)); }
void *finch_zalloc(int64_t n) { return must(calloc(1, n ? (size_t)n : 1)); }
// ---------- strings ----------
static void str_heap(FStr *out, const char *p, int64_t len) {
char *m = finch_alloc(len + 1);
if (len) memcpy(m, p, (size_t)len);
m[len] = 0;
out->ptr = m;
out->len = len;
out->cap = len + 1;
}
static void str_static(FStr *out, const char *lit) {
out->ptr = (char *)lit;
out->len = (int64_t)strlen(lit);
out->cap = 0;
}
// A copy that can be kept: literals are shared, everything else is duplicated.
void finch_str_copy(FStr *out, const FStr *s) {
if (s->cap == 0) *out = *s;
else str_heap(out, s->ptr, s->len);
}
// Make `s` writable (before s[i] = c).
void finch_str_own(FStr *s) {
if (s->cap > 0) return;
FStr t;
str_heap(&t, s->ptr, s->len);
*s = t;
}
void finch_str_drop(FStr *s) {
if (s->cap > 0) free(s->ptr);
}
void finch_str_from_c(FStr *out, const char *p) {
if (!p) {
str_static(out, empty);
return;
}
out->ptr = (char *)p;
out->len = (int64_t)strlen(p);
out->cap = -1;
}
void finch_str_concat(FStr *out, const FStr *a, const FStr *b) {
int64_t len = a->len + b->len;
char *m = finch_alloc(len + 1);
memcpy(m, a->ptr, (size_t)a->len);
memcpy(m + a->len, b->ptr, (size_t)b->len);
m[len] = 0;
out->ptr = m;
out->len = len;
out->cap = len + 1;
}
int finch_str_eq(const FStr *a, const FStr *b) {
return a->len == b->len && memcmp(a->ptr, b->ptr, (size_t)a->len) == 0;
}
int64_t finch_str_cmp(const FStr *a, const FStr *b) {
int64_t n = a->len < b->len ? a->len : b->len;
int c = memcmp(a->ptr, b->ptr, (size_t)n);
if (c) return c < 0 ? -1 : 1;
return a->len < b->len ? -1 : a->len > b->len;
}
void finch_str_from_int(FStr *out, int64_t v) {
char buf[32];
int n = snprintf(buf, sizeof buf, "%lld", (long long)v);
str_heap(out, buf, n);
}
void finch_str_from_uint(FStr *out, uint64_t v) {
char buf[32];
int n = snprintf(buf, sizeof buf, "%llu", (unsigned long long)v);
str_heap(out, buf, n);
}
void finch_str_from_float(FStr *out, double v) {
char buf[64];
int n = snprintf(buf, sizeof buf, "%g", v);
str_heap(out, buf, n);
}
void finch_str_from_char(FStr *out, char c) { str_heap(out, &c, 1); }
void finch_str_from_bool(FStr *out, int b) { str_static(out, b ? "true" : "false"); }
static void bad_number(const FStr *s, const char *kind, const char *file, int64_t line) {
char msg[200];
snprintf(msg, sizeof msg, "can't turn \"%.*s\" into %s", (int)(s->len > 100 ? 100 : s->len), s->ptr, kind);
finch_panic(file, line, msg);
}
int64_t finch_str_to_int(const FStr *s, const char *file, int64_t line) {
const char *p = s->ptr;
while (isspace((unsigned char)*p)) p++;
char *end;
errno = 0;
long long v = strtoll(p, &end, 10);
while (isspace((unsigned char)*end)) end++;
if (end == p || *end || errno == ERANGE) bad_number(s, "int", file, line);
return v;
}
double finch_str_to_float(const FStr *s, const char *file, int64_t line) {
const char *p = s->ptr;
while (isspace((unsigned char)*p)) p++;
char *end;
double v = strtod(p, &end);
while (isspace((unsigned char)*end)) end++;
if (end == p || *end) bad_number(s, "float", file, line);
return v;
}
void finch_str_sub(FStr *out, const FStr *s, int64_t start, int64_t end, const char *file, int64_t line) {
if (start < 0 || end > s->len || start > end) {
char msg[160];
snprintf(msg, sizeof msg, "sub(%lld, %lld) is out of range (the length is %lld)", (long long)start,
(long long)end, (long long)s->len);
finch_panic(file, line, msg);
}
str_heap(out, s->ptr + start, end - start);
}
int64_t finch_str_find(const FStr *s, const FStr *t) {
if (t->len == 0) return 0;
for (int64_t i = 0; i + t->len <= s->len; i++)
if (s->ptr[i] == t->ptr[0] && memcmp(s->ptr + i, t->ptr, (size_t)t->len) == 0) return i;
return -1;
}
int finch_str_starts(const FStr *s, const FStr *t) {
return t->len <= s->len && memcmp(s->ptr, t->ptr, (size_t)t->len) == 0;
}
int finch_str_ends(const FStr *s, const FStr *t) {
return t->len <= s->len && memcmp(s->ptr + s->len - t->len, t->ptr, (size_t)t->len) == 0;
}
void finch_str_trim(FStr *out, const FStr *s) {
int64_t a = 0, b = s->len;
while (a < b && isspace((unsigned char)s->ptr[a])) a++;
while (b > a && isspace((unsigned char)s->ptr[b - 1])) b--;
str_heap(out, s->ptr + a, b - a);
}
void finch_str_upper(FStr *out, const FStr *s) {
str_heap(out, s->ptr, s->len);
for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)toupper((unsigned char)out->ptr[i]);
}
void finch_str_lower(FStr *out, const FStr *s) {
str_heap(out, s->ptr, s->len);
for (int64_t i = 0; i < out->len; i++) out->ptr[i] = (char)tolower((unsigned char)out->ptr[i]);
}
void finch_str_replace(FStr *out, const FStr *s, const FStr *from, const FStr *to) {
if (from->len == 0) {
str_heap(out, s->ptr, s->len);
return;
}
int64_t count = 0;
for (int64_t i = 0; i + from->len <= s->len;) {
if (memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {
count++;
i += from->len;
} else {
i++;
}
}
int64_t len = s->len + count * (to->len - from->len);
char *m = finch_alloc(len + 1), *w = m;
for (int64_t i = 0; i < s->len;) {
if (i + from->len <= s->len && memcmp(s->ptr + i, from->ptr, (size_t)from->len) == 0) {
memcpy(w, to->ptr, (size_t)to->len);
w += to->len;
i += from->len;
} else {
*w++ = s->ptr[i++];
}
}
*w = 0;
out->ptr = m;
out->len = len;
out->cap = len + 1;
}
static void arr_push_str(FArr *a, const char *p, int64_t len);
// "a,b,,c".split(",") -> ["a", "b", "", "c"]; split("") gives single characters.
void finch_str_split(FArr *out, const FStr *s, const FStr *sep) {
out->ptr = NULL;
out->len = out->cap = 0;
if (sep->len == 0) {
for (int64_t i = 0; i < s->len; i++) arr_push_str(out, s->ptr + i, 1);
return;
}
int64_t start = 0;
for (int64_t i = 0; i + sep->len <= s->len;) {
if (memcmp(s->ptr + i, sep->ptr, (size_t)sep->len) == 0) {
arr_push_str(out, s->ptr + start, i - start);
i += sep->len;
start = i;
} else {
i++;
}
}
arr_push_str(out, s->ptr + start, s->len - start);
}
void finch_str_join(FStr *out, const FArr *parts, const FStr *sep) {
const FStr *p = parts->ptr;
int64_t len = 0;
for (int64_t i = 0; i < parts->len; i++) len += p[i].len + (i ? sep->len : 0);
char *m = finch_alloc(len + 1), *w = m;
for (int64_t i = 0; i < parts->len; i++) {
if (i) {
memcpy(w, sep->ptr, (size_t)sep->len);
w += sep->len;
}
memcpy(w, p[i].ptr, (size_t)p[i].len);
w += p[i].len;
}
*w = 0;
out->ptr = m;
out->len = len;
out->cap = len + 1;
}
void finch_str_repeat(FStr *out, const FStr *s, int64_t n) {
if (n < 0) n = 0;
int64_t len = s->len * n;
char *m = finch_alloc(len + 1);
for (int64_t i = 0; i < n; i++) memcpy(m + i * s->len, s->ptr, (size_t)s->len);
m[len] = 0;
out->ptr = m;
out->len = len;
out->cap = len + 1;
}
void finch_str_from_bytes(FStr *out, const FArr *bytes) { str_heap(out, bytes->ptr, bytes->len); }
// ---------- arrays ----------
void finch_arr_reserve(FArr *a, int64_t need, int64_t es) {
if (need <= a->cap) return;
int64_t cap = a->cap * 2;
if (cap < 4) cap = 4;
if (cap < need) cap = need;
a->ptr = must(realloc(a->cap > 0 ? a->ptr : NULL, (size_t)(cap * es)));
a->cap = cap;
}
void finch_arr_make(FArr *out, int64_t len, int64_t es) {
if (len < 0) len = 0;
out->ptr = len ? finch_zalloc(len * es) : NULL;
out->len = len;
out->cap = len;
}
// grow or shrink to `len`; new elements are zero
void finch_arr_resize(FArr *a, int64_t len, int64_t es) {
if (len < 0) len = 0;
finch_arr_reserve(a, len, es);
if (len > a->len) memset((char *)a->ptr + a->len * es, 0, (size_t)((len - a->len) * es));
a->len = len;
}
// a byte-for-byte copy of the elements (the compiler deep-copies owning elements afterwards)
void finch_arr_clone_raw(FArr *out, const FArr *a, int64_t es) {
out->len = a->len;
out->cap = a->len;
out->ptr = a->len ? finch_alloc(a->len * es) : NULL;
if (a->len) memcpy(out->ptr, a->ptr, (size_t)(a->len * es));
}
void finch_arr_free(FArr *a) {
if (a->cap > 0) free(a->ptr);
}
// open a hole at i (0 <= i <= len), len grows by one
void finch_arr_insert_gap(FArr *a, int64_t i, int64_t es, const char *file, int64_t line) {
if (i < 0 || i > a->len) finch_panic_index(file, line, i, a->len);
finch_arr_reserve(a, a->len + 1, es);
char *p = a->ptr;
memmove(p + (i + 1) * es, p + i * es, (size_t)((a->len - i) * es));
a->len++;
}
// close the hole at i (the element was already moved out), len shrinks by one
void finch_arr_remove_gap(FArr *a, int64_t i, int64_t es) {
char *p = a->ptr;
memmove(p + i * es, p + (i + 1) * es, (size_t)((a->len - i - 1) * es));
a->len--;
}
static void arr_push_str(FArr *a, const char *p, int64_t len) {
finch_arr_reserve(a, a->len + 1, (int64_t)sizeof(FStr));
str_heap((FStr *)a->ptr + a->len, p, len);
a->len++;
}
void finch_bytes_from_str(FArr *out, const FStr *s) {
out->len = out->cap = s->len;
out->ptr = s->len ? finch_alloc(s->len) : NULL;
if (s->len) memcpy(out->ptr, s->ptr, (size_t)s->len);
}
// ---------- program arguments, input and files ----------
void finch_args(FArr *out, int argc, char **argv) {
out->len = out->cap = argc;
out->ptr = finch_alloc((int64_t)sizeof(FStr) * (argc ? argc : 1));
for (int i = 0; i < argc; i++) str_heap((FStr *)out->ptr + i, argv[i], (int64_t)strlen(argv[i]));
}
// input("Name? ") prints the prompt and reads one line, without the newline.
void finch_input(FStr *out, const FStr *prompt) {
if (prompt->len) fwrite(prompt->ptr, 1, (size_t)prompt->len, stdout);
fflush(stdout);
char *line = NULL;
size_t size = 0;
ssize_t n = getline(&line, &size, stdin);
if (n < 0) {
free(line);
str_static(out, empty);
return;
}
while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = 0;
out->ptr = line;
out->len = n;
out->cap = (int64_t)size;
}
int finch_file_exists(const FStr *path) {
FILE *f = fopen(path->ptr, "rb");
if (f) fclose(f);
return f != NULL;
}
void finch_read_file(FStr *out, const FStr *path, const char *file, int64_t line) {
FILE *f = fopen(path->ptr, "rb");
if (!f) {
char msg[300];
snprintf(msg, sizeof msg, "can't read the file \"%.*s\": %s", (int)(path->len > 200 ? 200 : path->len),
path->ptr, strerror(errno));
finch_panic(file, line, msg);
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *m = finch_alloc(size + 1);
size_t got = fread(m, 1, (size_t)size, f);
fclose(f);
m[got] = 0;
out->ptr = m;
out->len = (int64_t)got;
out->cap = size + 1;
}
int finch_write_file(const FStr *path, const FStr *text) {
FILE *f = fopen(path->ptr, "wb");
if (!f) return 0;
size_t put = fwrite(text->ptr, 1, (size_t)text->len, f);
return fclose(f) == 0 && put == (size_t)text->len;
}

View file

@ -2,10 +2,10 @@
pkgs.mkShell { pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
# building Finch # building Finch (the clang wrapper first, so `clang` finds the system headers)
llvmPackages.clang
llvmPackages.llvm llvmPackages.llvm
llvmPackages.libclang # reads C headers for `import "stdio.h"` llvmPackages.libclang # reads C headers for `import "stdio.h"`
llvmPackages.clang
cmake cmake
ninja ninja
pkg-config # finds the right flags for `link "..."` pkg-config # finds the right flags for `link "..."`

309
src/abi.cpp Normal file
View file

@ -0,0 +1,309 @@
// The C calling convention for structs passed and returned by value (System V x86-64),
// the way clang does it: small structs travel in registers, split into eightbytes that are
// either INTEGER (general registers) or SSE (xmm registers); bigger ones go through memory.
#include "codegen_impl.h"
using namespace llvm;
namespace finch {
namespace {
enum Class { None, Integer, Sse };
struct Leaf {
uint64_t offset;
uint64_t size;
bool isFloat;
bool isDouble;
};
} // namespace
// Flatten a value into its scalar pieces with byte offsets.
static void leaves(Codegen &cg, const FType &t, uint64_t base, std::vector<Leaf> &out) {
switch (t.kind) {
case FType::Struct: {
const StructLayout *lay = cg.dl->getStructLayout(t.info->llvm);
for (auto &f : t.info->fields) leaves(cg, f.type, base + lay->getElementOffset(f.llvmIndex), out);
break;
}
case FType::Fixed: {
uint64_t es = cg.dl->getTypeAllocSize(cg.ty(*t.elem));
for (long long i = 0; i < t.count; i++) leaves(cg, *t.elem, base + i * es, out);
break;
}
default:
out.push_back({base, cg.dl->getTypeAllocSize(cg.ty(t)), t.kind == FType::F32 || t.kind == FType::F64,
t.kind == FType::F64});
}
}
AbiArg Codegen::classify(const FType &t) {
AbiArg a;
if (t.kind != FType::Struct) return a; // Direct
uint64_t size = dl->getTypeAllocSize(ty(t));
if (size > 16 || size == 0) {
a.kind = AbiArg::Memory;
return a;
}
std::vector<Leaf> ls;
leaves(*this, t, 0, ls);
unsigned n = (size + 7) / 8;
Class cls[2] = {None, None};
bool hasDouble[2] = {false, false};
for (const Leaf &l : ls) {
if (l.offset % std::min<uint64_t>(l.size, 8) != 0) { // misaligned field (packed struct): memory
a.kind = AbiArg::Memory;
return a;
}
unsigned k = l.offset / 8;
cls[k] = (cls[k] == Integer || !l.isFloat) ? Integer : Sse;
hasDouble[k] |= l.isDouble;
}
a.kind = AbiArg::Expand;
for (unsigned k = 0; k < n; k++) {
uint64_t bytes = std::min<uint64_t>(8, size - 8 * k);
if (cls[k] == Sse) {
if (hasDouble[k]) a.parts.push_back(b.getDoubleTy());
else if (bytes > 4) a.parts.push_back(FixedVectorType::get(b.getFloatTy(), 2));
else a.parts.push_back(b.getFloatTy());
} else {
a.parts.push_back(b.getIntNTy(bytes * 8));
}
}
a.coerced = a.parts.size() == 1 ? a.parts[0] : StructType::get(ctx, a.parts);
return a;
}
namespace {
struct Plan {
AbiArg ret;
std::vector<AbiArg> params;
llvm::FunctionType *fty = nullptr;
};
} // namespace
// Decide how every parameter travels, counting the 6 integer and 8 SSE argument registers.
static Plan makePlan(Codegen &cg, const FType &ret, const std::vector<FType> &params, bool variadic,
std::vector<llvm::Type *> *extraTypes = nullptr) {
Plan pl;
int intRegs = 6, sseRegs = 8;
std::vector<llvm::Type *> types;
llvm::Type *retTy = cg.ty(ret);
if (ret.kind == FType::Str) retTy = cg.b.getPtrTy();
pl.ret = cg.classify(ret);
if (pl.ret.kind == AbiArg::Memory) {
types.push_back(cg.b.getPtrTy()); // sret
retTy = cg.b.getVoidTy();
intRegs--;
} else if (pl.ret.kind == AbiArg::Expand) {
retTy = pl.ret.coerced;
}
for (const FType &t : params) {
AbiArg a = cg.classify(t);
if (a.kind == AbiArg::Expand) {
int ni = 0, ns = 0;
for (llvm::Type *p : a.parts) (p->isIntegerTy() ? ni : ns)++;
if (ni <= intRegs && ns <= sseRegs) {
intRegs -= ni;
sseRegs -= ns;
for (llvm::Type *p : a.parts) types.push_back(p);
} else {
a.kind = AbiArg::Memory; // out of registers: the whole struct goes on the stack
}
}
if (a.kind == AbiArg::Memory) types.push_back(cg.b.getPtrTy());
if (a.kind == AbiArg::Direct) {
if (t.isFloat()) sseRegs--;
else intRegs--;
types.push_back(t.kind == FType::Str ? cg.b.getPtrTy() : cg.ty(t));
}
pl.params.push_back(a);
}
(void)extraTypes;
pl.fty = FunctionType::get(retTy, types, variadic);
return pl;
}
static Attribute::AttrKind extAttr(const FType &t) {
if (t.bits() >= 32 || !(t.isInt() || t.kind == FType::Bool || t.kind == FType::Char)) return Attribute::None;
return t.isSigned() || t.kind == FType::Char ? Attribute::SExt : Attribute::ZExt;
}
// Attributes that tell LLVM how C passes each piece: sret, byval, signext/zeroext.
static AttributeList attrsFor(Codegen &cg, const Plan &pl, const FType &ret, const std::vector<FType> &params) {
LLVMContext &ctx = cg.ctx;
AttributeList al;
unsigned idx = 0;
if (pl.ret.kind == AbiArg::Memory) {
al = al.addParamAttribute(ctx, idx, Attribute::getWithStructRetType(ctx, cg.ty(ret)));
idx++;
} else if (Attribute::AttrKind k = extAttr(ret); k != Attribute::None) {
al = al.addRetAttribute(ctx, k);
}
for (size_t i = 0; i < params.size(); i++) {
const AbiArg &a = pl.params[i];
if (a.kind == AbiArg::Memory) {
uint64_t align = std::max<uint64_t>(8, params[i].info->isC ? params[i].info->cdecl->align : 8);
al = al.addParamAttribute(ctx, idx, Attribute::getWithByValType(ctx, cg.ty(params[i])));
al = al.addParamAttribute(ctx, idx, Attribute::getWithAlignment(ctx, Align(align)));
idx++;
} else if (a.kind == AbiArg::Expand) {
idx += a.parts.size();
} else {
if (Attribute::AttrKind k = extAttr(params[i]); k != Attribute::None) al = al.addParamAttribute(ctx, idx, k);
idx++;
}
}
return al;
}
static std::vector<FType> resolvedParams(Codegen &cg, const CFunc &f) {
std::vector<FType> ps;
for (const FType &t : f.params) ps.push_back(cg.resolve(t, Pos{}));
return ps;
}
Function *Codegen::declareC(const CFunc &f) {
if (Function *fn = mod->getFunction(f.name)) return fn;
FType ret = resolve(f.ret, Pos{});
std::vector<FType> ps = resolvedParams(*this, f);
for (const FType &t : ps)
if (t.kind == FType::Struct && !t.info->unsupported.empty()) return nullptr;
Plan pl = makePlan(*this, ret, ps, f.variadic);
Function *fn = Function::Create(pl.fty, Function::ExternalLinkage, f.name, mod.get());
fn->setAttributes(attrsFor(*this, pl, ret, ps));
return fn;
}
// A stack slot big enough for both the struct and its register form.
static Value *abiSlot(Codegen &cg, const FType &t, const AbiArg &a) {
llvm::Type *st = cg.ty(t);
llvm::Type *big = a.coerced && cg.dl->getTypeAllocSize(a.coerced) > cg.dl->getTypeAllocSize(st) ? a.coerced : st;
return cg.tmpOf(big);
}
Value_ Codegen::emitCCall(const CFunc &f, Function *fn, std::vector<Value_> args, Pos p) {
FType ret = resolve(f.ret, p);
std::vector<FType> ps = resolvedParams(*this, f);
for (const FType &t : ps)
if (t.kind == FType::Struct && !t.info->unsupported.empty())
failAt(p.file, p.line, p.col, "the C function '" + f.name + "' takes the struct " + t.info->name +
" by value, which Finch can't pass yet: it " + t.info->unsupported);
if (ret.kind == FType::Struct && !ret.info->unsupported.empty())
failAt(p.file, p.line, p.col, "the C function '" + f.name + "' returns the struct " + ret.info->name +
" by value, which Finch can't receive yet: it " + ret.info->unsupported);
Plan pl = makePlan(*this, ret, ps, f.variadic);
std::vector<Value *> ll;
Value *sret = nullptr;
if (pl.ret.kind == AbiArg::Memory) {
sret = tmpOf(ty(ret));
cast<AllocaInst>(sret)->setAlignment(Align(16));
ll.push_back(sret);
}
for (size_t i = 0; i < args.size(); i++) {
const Value_ &v = args[i];
if (i >= ps.size()) { // the '...' part: C's default promotions
const FType &t = v.type;
Value *x = v.v;
if (t.kind == FType::F32) x = b.CreateFPExt(x, b.getDoubleTy());
else if (t.kind == FType::Str) x = cstr(x);
else if (t.kind == FType::Bool || ((t.isInt() || t.kind == FType::Char) && t.bits() < 32))
x = b.CreateIntCast(x, b.getInt32Ty(), t.isSigned() || t.kind == FType::Char);
ll.push_back(x);
continue;
}
const AbiArg &a = pl.params[i];
if (ps[i].kind == FType::Str) {
ll.push_back(cstr(v.v));
} else if (a.kind == AbiArg::Memory) {
Value *s = tmpOf(ty(ps[i]));
cast<AllocaInst>(s)->setAlignment(Align(16));
b.CreateStore(v.v, s);
ll.push_back(s);
} else if (a.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, ps[i], a);
b.CreateStore(v.v, s);
for (size_t k = 0; k < a.parts.size(); k++)
ll.push_back(b.CreateLoad(a.parts[k], b.CreateConstInBoundsGEP1_64(b.getInt8Ty(), s, 8 * k)));
} else {
ll.push_back(v.v);
}
}
CallInst *call = b.CreateCall(pl.fty, fn, ll);
AttributeList al = attrsFor(*this, pl, ret, ps);
call->setAttributes(al);
if (ret.kind == FType::Void) return {nullptr, FType::Void};
if (pl.ret.kind == AbiArg::Memory) return {b.CreateLoad(ty(ret), sret), ret};
if (pl.ret.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, ret, pl.ret);
b.CreateStore(call, s);
return {b.CreateLoad(ty(ret), s), ret};
}
if (ret.kind == FType::Str) return strFromC(call); // borrowed: copied if it is kept
return {call, ret};
}
// A C-callable door into a Finch function, for addr(fn) (callbacks like qsort's or libclang's).
Function *Codegen::cThunk(Fn &fn, Pos p) {
if (fn.cThunk) return fn.cThunk;
auto cOk = [](const FType &t) {
return t.isNumber() || t.kind == FType::Bool || t.kind == FType::Char || t.isPtr() || t.kind == FType::Void ||
(t.kind == FType::Struct && t.info->isC);
};
for (const FType &t : fn.params)
if (!cOk(t))
failAt(p.file, p.line, p.col, "addr(" + fn.decl->name + ") gives C a function pointer, so its parameters must be C types (numbers, ptr, C structs), not " + t.show());
if (!cOk(fn.ret)) failAt(p.file, p.line, p.col, "addr(" + fn.decl->name + "): a function given to C can't return " + fn.ret.show());
Plan pl = makePlan(*this, fn.ret, fn.params, false);
Function *th = Function::Create(pl.fty, Function::InternalLinkage, "finch.c." + fn.decl->name, mod.get());
th->setAttributes(attrsFor(*this, pl, fn.ret, fn.params));
fn.cThunk = th;
IRBuilderBase::InsertPointGuard guard(b);
DISubprogram *savedFn = diFn;
diFn = nullptr;
b.SetInsertPoint(BasicBlock::Create(ctx, "entry", th));
b.SetCurrentDebugLocation(DebugLoc());
unsigned ai = 0;
Value *sret = nullptr;
if (pl.ret.kind == AbiArg::Memory) sret = th->getArg(ai++);
std::vector<Value *> args;
for (size_t i = 0; i < fn.params.size(); i++) {
const AbiArg &a = pl.params[i];
if (a.kind == AbiArg::Memory) {
args.push_back(b.CreateLoad(ty(fn.params[i]), th->getArg(ai++)));
} else if (a.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, fn.params[i], a);
for (size_t k = 0; k < a.parts.size(); k++)
b.CreateStore(th->getArg(ai++), b.CreateConstInBoundsGEP1_64(b.getInt8Ty(), s, 8 * k));
args.push_back(b.CreateLoad(ty(fn.params[i]), s));
} else {
args.push_back(th->getArg(ai++));
}
}
Value *r = b.CreateCall(fn.llvm, args);
if (fn.ret.kind == FType::Void) {
b.CreateRetVoid();
} else if (pl.ret.kind == AbiArg::Memory) {
b.CreateStore(r, sret);
b.CreateRetVoid();
} else if (pl.ret.kind == AbiArg::Expand) {
Value *s = abiSlot(*this, fn.ret, pl.ret);
b.CreateStore(r, s);
b.CreateRet(b.CreateLoad(pl.ret.coerced, s));
} else {
b.CreateRet(r);
}
diFn = savedFn;
return th;
}
} // namespace finch

103
src/ast.h
View file

@ -4,10 +4,23 @@
#include <string> #include <string>
#include <vector> #include <vector>
struct StructInfo; // filled in by codegen: fields, LLVM type, ownership
struct Type { struct Type {
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null }; enum Kind {
Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
Str, Ptr, Null,
Array, // []T: growable, owns its elements
Fixed, // [N]T: only inside C structs
Struct, // a resolved struct
Named, // a struct name as written, before codegen resolves it
};
Kind kind = Void; Kind kind = Void;
std::shared_ptr<Type> elem; // what a Ptr points to; null for the untyped `ptr` std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed
long long count = 0; // Fixed
std::string name; // Named / Struct
std::string module; // Named: written qualifier ("" = none); Struct: defining module
StructInfo *info = nullptr; // Struct
Type() = default; Type() = default;
Type(Kind k) : kind(k) {} Type(Kind k) : kind(k) {}
@ -16,6 +29,11 @@ struct Type {
p.elem = std::make_shared<Type>(t); p.elem = std::make_shared<Type>(t);
return p; return p;
} }
static Type arrayOf(const Type &t) {
Type a(Array);
a.elem = std::make_shared<Type>(t);
return a;
}
bool isInt() const { return kind >= I8 && kind <= U64; } bool isInt() const { return kind >= I8 && kind <= U64; }
bool isSigned() const { return kind >= I8 && kind <= I64; } bool isSigned() const { return kind >= I8 && kind <= I64; }
@ -35,13 +53,21 @@ struct Type {
bool operator==(const Type &o) const { bool operator==(const Type &o) const {
if (kind != o.kind) return false; if (kind != o.kind) return false;
if (kind != Ptr) return true; switch (kind) {
if (!elem || !o.elem) return !elem && !o.elem; case Ptr:
return *elem == *o.elem; if (!elem || !o.elem) return !elem && !o.elem;
return *elem == *o.elem;
case Array: return *elem == *o.elem;
case Fixed: return count == o.count && *elem == *o.elem;
case Struct: return info == o.info;
case Named: return name == o.name && module == o.module;
default: return true;
}
} }
bool operator!=(const Type &o) const { return !(*this == o); } bool operator!=(const Type &o) const { return !(*this == o); }
std::string name() const { // How the type is written in Finch, for error messages.
std::string show() const {
switch (kind) { switch (kind) {
case Void: return "nothing"; case Void: return "nothing";
case Bool: return "bool"; case Bool: return "bool";
@ -57,8 +83,12 @@ struct Type {
case F32: return "f32"; case F32: return "f32";
case F64: return "float"; case F64: return "float";
case Str: return "str"; case Str: return "str";
case Ptr: return elem ? "ptr[" + elem->name() + "]" : "ptr"; case Ptr: return elem ? "ptr[" + elem->show() + "]" : "ptr";
case Null: return "null"; case Null: return "null";
case Array: return "[]" + elem->show();
case Fixed: return "[" + std::to_string(count) + "]" + elem->show();
case Struct:
case Named: return module.empty() ? name : module + "." + name;
} }
return "?"; return "?";
} }
@ -69,11 +99,12 @@ bool typeFromName(const std::string &name, Type &out);
struct Pos { struct Pos {
int line = 0, col = 0; int line = 0, col = 0;
int file = 0; // index into the list of source files (see error.h)
}; };
// ---------- expressions ---------- // ---------- expressions ----------
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member }; enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member, Index, ArrayLit, Method };
struct Expr { struct Expr {
ExprKind kind; ExprKind kind;
@ -127,21 +158,39 @@ struct BinaryExpr : Expr {
BinaryExpr(Pos p, BinOp o, ExprPtr l, ExprPtr r) BinaryExpr(Pos p, BinOp o, ExprPtr l, ExprPtr r)
: Expr(ExprKind::Binary, p), op(o), lhs(std::move(l)), rhs(std::move(r)) {} : Expr(ExprKind::Binary, p), op(o), lhs(std::move(l)), rhs(std::move(r)) {}
}; };
// name(args): a function, a built-in, a conversion or a struct constructor.
struct CallExpr : Expr { struct CallExpr : Expr {
std::string callee; std::string callee;
std::vector<ExprPtr> args; std::vector<ExprPtr> args;
std::vector<std::string> argNames; // "" for positional; Point(x: 1, y: 2)
CallExpr(Pos p, std::string c) : Expr(ExprKind::Call, p), callee(std::move(c)) {} CallExpr(Pos p, std::string c) : Expr(ExprKind::Call, p), callee(std::move(c)) {}
}; };
struct MemberExpr : Expr { // obj.field (for now only p.value) struct MemberExpr : Expr { // obj.field, p.value, a.len
ExprPtr obj; ExprPtr obj;
std::string field; std::string field;
MemberExpr(Pos p, ExprPtr o, std::string f) MemberExpr(Pos p, ExprPtr o, std::string f)
: Expr(ExprKind::Member, p), obj(std::move(o)), field(std::move(f)) {} : Expr(ExprKind::Member, p), obj(std::move(o)), field(std::move(f)) {}
}; };
struct IndexExpr : Expr { // a[i]
ExprPtr obj, index;
IndexExpr(Pos p, ExprPtr o, ExprPtr i) : Expr(ExprKind::Index, p), obj(std::move(o)), index(std::move(i)) {}
};
struct ArrayLitExpr : Expr { // [1, 2, 3]
std::vector<ExprPtr> elems;
explicit ArrayLitExpr(Pos p) : Expr(ExprKind::ArrayLit, p) {}
};
// obj.name(args): a.push(x), s.find("x"), and math.sqrt(2) / math.Vec(1, 2) for modules
struct MethodExpr : Expr {
ExprPtr obj;
std::string name;
std::vector<ExprPtr> args;
std::vector<std::string> argNames;
MethodExpr(Pos p, ExprPtr o, std::string n) : Expr(ExprKind::Method, p), obj(std::move(o)), name(std::move(n)) {}
};
// ---------- statements ---------- // ---------- statements ----------
enum class StmtKind { VarDecl, Assign, Expr, If, While, For, Return, Break, Continue, Block }; enum class StmtKind { VarDecl, Assign, Expr, If, While, For, ForEach, Return, Break, Continue, Block, Defer };
struct Stmt { struct Stmt {
StmtKind kind; StmtKind kind;
@ -165,7 +214,7 @@ struct VarDeclStmt : Stmt {
VarDeclStmt(Pos p) : Stmt(StmtKind::VarDecl, p) {} VarDeclStmt(Pos p) : Stmt(StmtKind::VarDecl, p) {}
}; };
struct AssignStmt : Stmt { struct AssignStmt : Stmt {
ExprPtr target; // a variable or p.value ExprPtr target; // a variable, field, element or p.value
char op; // '=', '+', '-', '*', '/', '%' char op; // '=', '+', '-', '*', '/', '%'
ExprPtr value; ExprPtr value;
AssignStmt(Pos p) : Stmt(StmtKind::Assign, p) {} AssignStmt(Pos p) : Stmt(StmtKind::Assign, p) {}
@ -191,6 +240,12 @@ struct ForStmt : Stmt {
BlockPtr body; BlockPtr body;
ForStmt(Pos p) : Stmt(StmtKind::For, p) {} ForStmt(Pos p) : Stmt(StmtKind::For, p) {}
}; };
struct ForEachStmt : Stmt {
std::string var;
ExprPtr list; // for var in list (an array or a str)
BlockPtr body;
ForEachStmt(Pos p) : Stmt(StmtKind::ForEach, p) {}
};
struct ReturnStmt : Stmt { struct ReturnStmt : Stmt {
ExprPtr value; // may be null ExprPtr value; // may be null
ReturnStmt(Pos p) : Stmt(StmtKind::Return, p) {} ReturnStmt(Pos p) : Stmt(StmtKind::Return, p) {}
@ -201,6 +256,10 @@ struct BreakStmt : Stmt {
struct ContinueStmt : Stmt { struct ContinueStmt : Stmt {
explicit ContinueStmt(Pos p) : Stmt(StmtKind::Continue, p) {} explicit ContinueStmt(Pos p) : Stmt(StmtKind::Continue, p) {}
}; };
struct DeferStmt : Stmt { // defer <statement>: runs when the block ends
StmtPtr body;
DeferStmt(Pos p, StmtPtr s) : Stmt(StmtKind::Defer, p), body(std::move(s)) {}
};
// ---------- top level ---------- // ---------- top level ----------
@ -218,18 +277,36 @@ struct FnDecl {
BlockPtr body; BlockPtr body;
}; };
struct Field {
Type type;
std::string name;
ExprPtr init; // default value, may be null
Pos pos;
};
struct StructDecl {
Pos pos;
std::string name;
std::vector<Field> fields;
};
struct Import { struct Import {
Pos pos; Pos pos;
std::string path; // "stdio.h" for C headers std::string path; // "stdio.h" (C header, isC) or "math" (Finch module)
bool isC = true;
}; };
struct Link { struct Link {
Pos pos; Pos pos;
std::string lib; // "glfw" -> libglfw.so std::string lib; // "glfw" -> libglfw.so; "helpers.c" / ".o" / ".a" -> a file next to the .fn file
}; };
// One .fn file.
struct Program { struct Program {
std::string module; // "" for the main file, else the module's name
std::string path;
std::vector<Import> imports; std::vector<Import> imports;
std::vector<Link> links; std::vector<Link> links;
std::vector<StructDecl> structs;
std::vector<FnDecl> fns; std::vector<FnDecl> fns;
}; };

View file

@ -41,189 +41,300 @@ std::string str(CXString s) {
return out; return out;
} }
// C type -> Finch type. Returns false (with a reason) if Finch can't express it yet. // Where a C type is used decides how `char *` looks in Finch: in function signatures it is
bool mapType(CXType t, Type &out, std::string &why) { // a `str`; inside structs, globals and behind pointers it stays a raw `ptr[char]`.
t = clang_getCanonicalType(t); enum class Ctx { Signature, Data };
long long size = clang_Type_getSizeOf(t);
auto sized = [&](bool isSigned) {
switch (size) {
case 1: out = isSigned ? Type::I8 : Type::U8; return true;
case 2: out = isSigned ? Type::I16 : Type::U16; return true;
case 4: out = isSigned ? Type::I32 : Type::U32; return true;
case 8: out = isSigned ? Type::I64 : Type::U64; return true;
}
why = "uses a " + std::to_string(size * 8) + "-bit number";
return false;
};
switch (t.kind) { struct Importer {
case CXType_Void: out = Type::Void; return true; CImports &out;
case CXType_Bool: out = Type::Bool; return true; CXTranslationUnit tu = nullptr;
case CXType_Char_S: std::string header;
case CXType_Char_U: out = Type::Char; return true; std::map<std::string, std::string> recordNames; // record USR -> Finch name (typedef wins)
case CXType_SChar: std::map<std::string, int> defining; // USR -> 1 while its fields are being mapped
case CXType_Short: int anon = 0;
case CXType_Int:
case CXType_Long: explicit Importer(CImports &o) : out(o) {}
case CXType_LongLong: return sized(true);
case CXType_UChar: // The Finch name of a C record: its typedef name, its tag, or a made-up one.
case CXType_UShort: std::string recordName(CXType t) {
case CXType_UInt: CXCursor decl = clang_getTypeDeclaration(t);
case CXType_ULong: std::string usr = str(clang_getCursorUSR(decl));
case CXType_ULongLong: return sized(false); auto it = recordNames.find(usr);
case CXType_Enum: return sized(true); if (it != recordNames.end()) return it->second;
case CXType_Float: out = Type::F32; return true; std::string name = str(clang_getCursorSpelling(decl));
case CXType_Double: out = Type::F64; return true; if (name.empty() || name.find(' ') != std::string::npos) name = "anon_struct_" + std::to_string(++anon);
case CXType_Pointer: { recordNames[usr] = name;
CXType pointee = clang_getCanonicalType(clang_getPointeeType(t)); return name;
if (pointee.kind == CXType_Char_S || pointee.kind == CXType_Char_U) { }
out = Type::Str; // char* is a C string
// Fill out.structs[name] for a complete record; returns its name, or "" if it can't be one.
std::string defineRecord(CXType t) {
t = clang_getCanonicalType(t);
CXCursor decl = clang_getTypeDeclaration(t);
std::string usr = str(clang_getCursorUSR(decl));
std::string name = recordName(t);
if (out.structs.count(name) || defining.count(usr)) return name;
long long size = clang_Type_getSizeOf(t);
if (size < 0) return ""; // incomplete (opaque) struct
defining[usr] = 1;
CStruct s;
s.name = name;
s.size = size;
s.align = clang_Type_getAlignOf(t);
if (clang_getCursorKind(decl) == CXCursor_UnionDecl) s.unsupported = "is a C union";
struct Ctx2 { Importer *self; CStruct *s; } c2{this, &s};
clang_Type_visitFields(t, [](CXCursor field, CXClientData data) -> CXVisitorResult {
auto &c = *static_cast<Ctx2 *>(data);
CField f;
f.name = str(clang_getCursorSpelling(field));
CXType ft = clang_getCanonicalType(clang_getCursorType(field));
f.offset = clang_Cursor_getOffsetOfField(field) / 8;
f.size = clang_Type_getSizeOf(ft);
f.align = clang_Type_getAlignOf(ft);
std::string why;
if (clang_Cursor_isBitField(field)) {
c.s->unsupported = "has bit fields";
f.hidden = true;
} else if (ft.kind == CXType_Record &&
clang_getCursorKind(clang_getTypeDeclaration(ft)) == CXCursor_UnionDecl) {
f.hidden = true; // unions are kept as opaque bytes
} else if (!c.self->mapType(ft, f.type, why, Ctx::Data)) {
f.hidden = true;
}
if (f.hidden) c.s->hasHidden = true;
if (f.name.empty()) f.hidden = true;
c.s->fields.push_back(f);
return CXVisit_Continue;
}, &c2);
defining.erase(usr);
out.structs[name] = s;
return name;
}
// C type -> Finch type. Returns false (with a reason) if Finch can't express it.
bool mapType(CXType t, Type &res, std::string &why, Ctx ctx) {
t = clang_getCanonicalType(t);
long long size = clang_Type_getSizeOf(t);
auto sized = [&](bool isSigned) {
switch (size) {
case 1: res = isSigned ? Type::I8 : Type::U8; return true;
case 2: res = isSigned ? Type::I16 : Type::U16; return true;
case 4: res = isSigned ? Type::I32 : Type::U32; return true;
case 8: res = isSigned ? Type::I64 : Type::U64; return true;
}
why = "uses a " + std::to_string(size * 8) + "-bit number";
return false;
};
switch (t.kind) {
case CXType_Void: res = Type::Void; return true;
case CXType_Bool: res = Type::Bool; return true;
case CXType_Char_S:
case CXType_Char_U: res = Type::Char; return true;
case CXType_SChar:
case CXType_Short:
case CXType_Int:
case CXType_Long:
case CXType_LongLong: return sized(true);
case CXType_UChar:
case CXType_UShort:
case CXType_UInt:
case CXType_ULong:
case CXType_ULongLong: return sized(false);
case CXType_Enum: return sized(true);
case CXType_Float: res = Type::F32; return true;
case CXType_Double: res = Type::F64; return true;
case CXType_Pointer: {
CXType pointee = clang_getCanonicalType(clang_getPointeeType(t));
if ((pointee.kind == CXType_Char_S || pointee.kind == CXType_Char_U) && ctx == Ctx::Signature) {
res = Type::Str; // char* in a signature is a C string
return true;
}
Type inner;
std::string ignored;
if (pointee.kind == CXType_Void || pointee.kind == CXType_FunctionProto ||
pointee.kind == CXType_FunctionNoProto || !mapType(pointee, inner, ignored, Ctx::Data) ||
inner.kind == Type::Void || inner.kind == Type::Fixed)
res = Type::Ptr; // an opaque `ptr` you can pass around
else
res = Type::ptrTo(inner);
return true; return true;
} }
Type inner; case CXType_Record: {
std::string ignored; if (clang_getCursorKind(clang_getTypeDeclaration(t)) == CXCursor_UnionDecl) {
// pointers to structs, functions, void...: an opaque `ptr` you can pass around why = "uses a C union by value";
if (pointee.kind == CXType_Void || !mapType(pointee, inner, ignored) || inner.kind == Type::Void) return false;
out = Type::Ptr; }
else std::string name = defineRecord(t);
out = Type::ptrTo(inner); if (name.empty()) {
why = "uses an incomplete C struct by value";
return false;
}
res = Type(Type::Named);
res.name = name;
res.module = "C";
return true;
}
case CXType_ConstantArray: {
Type inner;
if (!mapType(clang_getArrayElementType(t), inner, why, Ctx::Data)) return false;
res = Type(Type::Fixed);
res.elem = std::make_shared<Type>(inner);
res.count = clang_getArraySize(t);
return true;
}
case CXType_LongDouble: why = "uses long double"; return false;
default: why = "uses the C type '" + str(clang_getTypeSpelling(t)) + "'"; return false;
}
}
// Simple #defines: a single number, maybe negative, maybe in parentheses.
bool macroValue(CXCursor c, CConst &res) {
CXToken *toks;
unsigned n;
clang_tokenize(tu, clang_getCursorExtent(c), &toks, &n);
std::vector<std::string> parts;
for (unsigned k = 1; k < n; k++) { // token 0 is the macro's name
if (clang_getTokenKind(toks[k]) == CXToken_Comment) continue;
parts.push_back(str(clang_getTokenSpelling(tu, toks[k])));
}
clang_disposeTokens(tu, toks, n);
while (parts.size() >= 2 && parts.front() == "(" && parts.back() == ")") {
parts.erase(parts.begin());
parts.pop_back();
}
bool neg = false;
if (parts.size() == 2 && parts[0] == "-") {
neg = true;
parts.erase(parts.begin());
}
if (parts.size() != 1) return false;
const std::string &lit = parts[0];
if (lit.empty() || !(std::isdigit((unsigned char)lit[0]) || lit[0] == '.')) return false;
bool isFloat = lit.find_first_of(".eE") != std::string::npos && lit.rfind("0x", 0) != 0;
char *end;
if (isFloat) {
res.type = Type::F64;
res.f = std::strtod(lit.c_str(), &end);
if (neg) res.f = -res.f;
} else {
res.type = Type::I64;
res.i = (long long)std::strtoull(lit.c_str(), &end, 0); // C rules: 0x.., 0755
if (neg) res.i = -res.i;
}
for (; *end; end++) // allow suffixes: 10UL, 1.0f
if (!std::strchr("uUlLfF", *end)) return false;
return true; return true;
} }
case CXType_Record: why = "passes a C struct by value (structs are coming)"; return false;
case CXType_LongDouble: why = "uses long double"; return false;
default: why = "uses the C type '" + str(clang_getTypeSpelling(t)) + "'"; return false;
}
}
// Simple #defines: a single number, maybe negative, maybe in parentheses. void visitTop(CXCursor c) {
bool macroValue(CXTranslationUnit tu, CXCursor c, CConst &out) { std::string name = str(clang_getCursorSpelling(c));
CXToken *toks; switch (clang_getCursorKind(c)) {
unsigned n; case CXCursor_TypedefDecl: {
clang_tokenize(tu, clang_getCursorExtent(c), &toks, &n); // typedef struct {...} Name; -> the struct is called Name in Finch
std::vector<std::string> parts; CXType u = clang_getCanonicalType(clang_getTypedefDeclUnderlyingType(c));
for (unsigned k = 1; k < n; k++) { // token 0 is the macro's name if (u.kind == CXType_Record) {
if (clang_getTokenKind(toks[k]) == CXToken_Comment) continue; std::string usr = str(clang_getCursorUSR(clang_getTypeDeclaration(u)));
parts.push_back(str(clang_getTokenSpelling(tu, toks[k]))); // prefer the typedef name, unless the struct is already known under its tag
auto it = recordNames.find(usr);
bool used = it != recordNames.end() && out.structs.count(it->second);
if (!used && !out.structs.count(name)) recordNames[usr] = name;
}
break;
}
case CXCursor_FunctionDecl: {
if (out.fns.count(name)) break;
CFunc f;
f.name = name;
f.header = header;
if (clang_Cursor_getStorageClass(c) == CX_SC_Static) {
f.unsupported = "is a static inline C function (it has no code to link against)";
out.fns[name] = f;
break;
}
CXType ft = clang_getCanonicalType(clang_getCursorType(c));
if (ft.kind == CXType_FunctionNoProto) {
f.unsupported = "has no parameter list in its C declaration";
out.fns[name] = f;
break;
}
std::string why;
if (!mapType(clang_getResultType(ft), f.ret, why, Ctx::Signature)) f.unsupported = why;
int n = clang_getNumArgTypes(ft);
for (int k = 0; k < n && f.unsupported.empty(); k++) {
Type t;
if (!mapType(clang_getArgType(ft, k), t, why, Ctx::Signature)) f.unsupported = why;
f.params.push_back(t);
}
f.variadic = clang_isFunctionTypeVariadic(ft);
out.fns[name] = f;
break;
}
case CXCursor_VarDecl: {
if (clang_Cursor_getStorageClass(c) != CX_SC_Extern || out.globals.count(name)) break;
CGlobal g;
std::string why;
if (mapType(clang_getCursorType(c), g.type, why, Ctx::Data)) out.globals[name] = g;
break;
}
case CXCursor_StructDecl:
if (clang_isCursorDefinition(c) && !name.empty() && name.find(' ') == std::string::npos) {
std::string usr = str(clang_getCursorUSR(c));
if (!recordNames.count(usr)) recordNames[usr] = name;
}
break;
case CXCursor_EnumDecl:
clang_visitChildren(c, [](CXCursor k, CXCursor, CXClientData d) {
static_cast<Importer *>(d)->visitTop(k);
return CXChildVisit_Continue;
}, this);
break;
case CXCursor_EnumConstantDecl: {
CConst k;
k.type = Type::I64;
k.i = clang_getEnumConstantDeclValue(c);
out.consts.emplace(name, k);
break;
}
case CXCursor_MacroDefinition: {
if (clang_Cursor_isMacroBuiltin(c) || clang_Cursor_isMacroFunctionLike(c)) break;
CConst k;
if (macroValue(c, k)) out.consts.emplace(name, k);
break;
}
default: break;
}
} }
clang_disposeTokens(tu, toks, n);
while (parts.size() >= 2 && parts.front() == "(" && parts.back() == ")") {
parts.erase(parts.begin());
parts.pop_back();
}
bool neg = false;
if (parts.size() == 2 && parts[0] == "-") {
neg = true;
parts.erase(parts.begin());
}
if (parts.size() != 1) return false;
const std::string &lit = parts[0];
if (lit.empty() || !(std::isdigit((unsigned char)lit[0]) || lit[0] == '.')) return false;
bool isFloat = lit.find_first_of(".eE") != std::string::npos && lit.rfind("0x", 0) != 0;
char *end;
if (isFloat) {
out.type = Type::F64;
out.f = std::strtod(lit.c_str(), &end);
if (neg) out.f = -out.f;
} else {
out.type = Type::I64;
out.i = (long long)std::strtoull(lit.c_str(), &end, 0); // C rules: 0x.., 0755
if (neg) out.i = -out.i;
}
for (; *end; end++) // allow suffixes: 10UL, 1.0f
if (!std::strchr("uUlLfF", *end)) return false;
return true;
}
struct Visit {
CImports *out;
CXTranslationUnit tu;
std::string header;
}; };
CXChildVisitResult visit(CXCursor c, CXCursor, CXClientData data) {
Visit &v = *static_cast<Visit *>(data);
CImports &out = *v.out;
std::string name = str(clang_getCursorSpelling(c));
switch (clang_getCursorKind(c)) {
case CXCursor_FunctionDecl: {
if (out.fns.count(name)) break;
CFunc f;
f.name = name;
f.header = v.header;
if (clang_Cursor_getStorageClass(c) == CX_SC_Static) {
f.unsupported = "is a static inline C function (it has no code to link against)";
out.fns[name] = f;
break;
}
CXType ft = clang_getCanonicalType(clang_getCursorType(c));
if (ft.kind == CXType_FunctionNoProto) {
f.unsupported = "has no parameter list in its C declaration";
out.fns[name] = f;
break;
}
std::string why;
if (!mapType(clang_getResultType(ft), f.ret, why)) f.unsupported = why;
int n = clang_getNumArgTypes(ft);
for (int k = 0; k < n && f.unsupported.empty(); k++) {
Type t;
if (!mapType(clang_getArgType(ft, k), t, why)) f.unsupported = why;
f.params.push_back(t);
}
f.variadic = clang_isFunctionTypeVariadic(ft);
out.fns[name] = f;
break;
}
case CXCursor_VarDecl: {
if (clang_Cursor_getStorageClass(c) != CX_SC_Extern || out.globals.count(name)) break;
CGlobal g;
std::string why;
if (mapType(clang_getCursorType(c), g.type, why)) out.globals[name] = g;
break;
}
case CXCursor_EnumDecl:
clang_visitChildren(c, visit, data);
break;
case CXCursor_EnumConstantDecl: {
CConst k;
k.type = Type::I64;
k.i = clang_getEnumConstantDeclValue(c);
out.consts.emplace(name, k);
break;
}
case CXCursor_MacroDefinition: {
if (clang_Cursor_isMacroBuiltin(c) || clang_Cursor_isMacroFunctionLike(c)) break;
CConst k;
if (macroValue(v.tu, c, k)) out.consts.emplace(name, k);
break;
}
default: break;
}
return CXChildVisit_Continue;
}
} // namespace } // namespace
CImports importHeaders(const std::vector<Import> &imports, const std::string &dir) { CImports importHeaders(const std::vector<Import> &imports, const std::vector<std::string> &dirs) {
CImports out; CImports out;
if (imports.empty()) return out; bool any = false;
for (const Import &im : imports) any |= im.isC;
if (!any) return out;
std::vector<std::string> args = {"-x", "c", "-std=gnu11", "-I" + dir}; std::vector<std::string> args = {"-x", "c", "-std=gnu11"};
for (const std::string &d : dirs) args.push_back("-I" + d);
for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d); for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d);
std::vector<const char *> argv; std::vector<const char *> argv;
for (const std::string &a : args) argv.push_back(a.c_str()); for (const std::string &a : args) argv.push_back(a.c_str());
Importer imp(out);
CXIndex index = clang_createIndex(0, 0); CXIndex index = clang_createIndex(0, 0);
for (const Import &im : imports) { for (const Import &im : imports) {
if (!im.isC) continue;
std::string src = "#include \"" + im.path + "\"\n"; std::string src = "#include \"" + im.path + "\"\n";
CXUnsavedFile file = {"finch_import.c", src.c_str(), (unsigned long)src.size()}; CXUnsavedFile file = {"finch_import.c", src.c_str(), (unsigned long)src.size()};
CXTranslationUnit tu = nullptr; CXTranslationUnit tu = nullptr;
CXErrorCode err = clang_parseTranslationUnit2( CXErrorCode err = clang_parseTranslationUnit2(
index, "finch_import.c", argv.data(), (int)argv.size(), &file, 1, index, "finch_import.c", argv.data(), (int)argv.size(), &file, 1,
CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies, &tu); CXTranslationUnit_DetailedPreprocessingRecord | CXTranslationUnit_SkipFunctionBodies, &tu);
if (err != CXError_Success || !tu) fail(im.pos.line, im.pos.col, "couldn't read C header '" + im.path + "'"); if (err != CXError_Success || !tu) failAt(im.pos.file, im.pos.line, im.pos.col, "couldn't read C header '" + im.path + "'");
for (unsigned k = 0; k < clang_getNumDiagnostics(tu); k++) { for (unsigned k = 0; k < clang_getNumDiagnostics(tu); k++) {
CXDiagnostic d = clang_getDiagnostic(tu, k); CXDiagnostic d = clang_getDiagnostic(tu, k);
@ -231,14 +342,18 @@ CImports importHeaders(const std::vector<Import> &imports, const std::string &di
std::string msg = str(clang_getDiagnosticSpelling(d)); std::string msg = str(clang_getDiagnosticSpelling(d));
clang_disposeDiagnostic(d); clang_disposeDiagnostic(d);
if (msg.find("file not found") != std::string::npos) if (msg.find("file not found") != std::string::npos)
fail(im.pos.line, im.pos.col, "can't find the C header '" + im.path + "'"); failAt(im.pos.file, im.pos.line, im.pos.col, "can't find the C header '" + im.path + "'");
fail(im.pos.line, im.pos.col, "C header '" + im.path + "' has an error: " + msg); failAt(im.pos.file, im.pos.line, im.pos.col, "C header '" + im.path + "' has an error: " + msg);
} }
clang_disposeDiagnostic(d); clang_disposeDiagnostic(d);
} }
Visit v{&out, tu, im.path}; imp.tu = tu;
clang_visitChildren(clang_getTranslationUnitCursor(tu), visit, &v); imp.header = im.path;
clang_visitChildren(clang_getTranslationUnitCursor(tu), [](CXCursor c, CXCursor, CXClientData d) {
static_cast<Importer *>(d)->visitTop(c);
return CXChildVisit_Continue;
}, &imp);
clang_disposeTranslationUnit(tu); clang_disposeTranslationUnit(tu);
} }
clang_disposeIndex(index); clang_disposeIndex(index);

View file

@ -2,11 +2,13 @@
#include "ast.h" #include "ast.h"
#include <map>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
// What Finch learned from `import "something.h"`. // What Finch learned from `import "something.h"`.
// C structs by value appear in types as Named{name, module "C"}.
struct CFunc { struct CFunc {
std::string name; std::string name;
@ -27,11 +29,28 @@ struct CGlobal { // extern variables like stdout
Type type; Type type;
}; };
struct CField {
std::string name;
Type type;
long long offset = 0; // bytes
long long size = 0, align = 1;
bool hidden = false; // unions, bitfields, unmappable types: kept for layout only
};
struct CStruct {
std::string name;
std::vector<CField> fields;
long long size = 0, align = 1;
std::string unsupported; // why it can't be used by value (pointers to it still work)
bool hasHidden = false;
};
struct CImports { struct CImports {
std::unordered_map<std::string, CFunc> fns; std::unordered_map<std::string, CFunc> fns;
std::unordered_map<std::string, CConst> consts; std::unordered_map<std::string, CConst> consts;
std::unordered_map<std::string, CGlobal> globals; std::unordered_map<std::string, CGlobal> globals;
std::map<std::string, CStruct> structs;
}; };
// `dir` is the folder of the .fn file, searched first for "local.h" headers. // `dirs`: folders searched first for "local.h" headers (the .fn files' folders).
CImports importHeaders(const std::vector<Import> &imports, const std::string &dir); CImports importHeaders(const std::vector<Import> &imports, const std::vector<std::string> &dirs);

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
#include <memory> #include <memory>
// All files of the program (the main file first, then its modules) become one LLVM module.
// `tm` describes the machine we compile for (sizes, alignment), which shapes the IR. // `tm` describes the machine we compile for (sizes, alignment), which shapes the IR.
std::unique_ptr<llvm::Module> generate(const Program &prog, const CImports &c, llvm::LLVMContext &ctx, std::unique_ptr<llvm::Module> generate(const std::vector<Program> &progs, const CImports &c, llvm::LLVMContext &ctx,
llvm::TargetMachine &tm); llvm::TargetMachine &tm, bool debug);

703
src/codegen_builtins.cpp Normal file
View file

@ -0,0 +1,703 @@
// Code generation, part 3: calls, built-in functions, conversions and methods.
#include "codegen_impl.h"
using namespace llvm;
namespace finch {
bool Codegen::isBuiltin(const std::string &n) {
static const std::set<std::string> names = {"print", "addr", "input", "new", "free", "read_file", "write_file",
"file_exists", "exit"};
FType t;
return names.count(n) || typeFromName(n, t);
}
// Help people who forgot an import.
std::string Codegen::headerHint(const std::string &name) {
static const std::unordered_map<std::string, const char *> known = {
{"printf", "stdio.h"}, {"puts", "stdio.h"}, {"putchar", "stdio.h"}, {"scanf", "stdio.h"},
{"fopen", "stdio.h"}, {"getchar", "stdio.h"}, {"sqrt", "math.h"}, {"pow", "math.h"},
{"sin", "math.h"}, {"cos", "math.h"}, {"floor", "math.h"}, {"fabs", "math.h"},
{"malloc", "stdlib.h"}, {"rand", "stdlib.h"}, {"srand", "stdlib.h"}, {"abs", "stdlib.h"},
{"atoi", "stdlib.h"}, {"strlen", "string.h"}, {"strcpy", "string.h"}, {"memcpy", "string.h"},
{"time", "time.h"}, {"sleep", "unistd.h"}, {"usleep", "unistd.h"},
};
auto it = known.find(name);
return it == known.end() ? "" : std::string(" (it's from C: add import \"") + it->second + "\" at the top)";
}
void Codegen::checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p,
const std::string &name, bool variadic) {
for (size_t i = 0; i < names.size(); i++)
if (!names[i].empty())
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "named arguments (" + names[i] + ": ...) are only for creating structs");
if (args.size() == want || (variadic && args.size() > want)) return;
failAt(p.file, p.line, p.col, "'" + name + "' takes " + (variadic ? "at least " : "") + std::to_string(want) +
" argument(s), but got " + std::to_string(args.size()));
}
Function *Codegen::libc(const char *name, llvm::Type *ret, std::vector<llvm::Type *> params, bool vararg) {
FunctionCallee c = mod->getOrInsertFunction(name, FunctionType::get(ret, params, vararg));
return cast<Function>(c.getCallee());
}
// The runtime (runtime/finch_rt.c), declared on first use.
Function *Codegen::rt(const std::string &name) {
if (Function *f = mod->getFunction(name)) return f;
llvm::Type *P = b.getPtrTy(), *I = b.getInt64Ty(), *I32 = b.getInt32Ty(), *D = b.getDoubleTy(), *V = b.getVoidTy();
struct Sig { llvm::Type *ret; std::vector<llvm::Type *> params; };
std::map<std::string, Sig> sigs = {
{"finch_panic", {V, {P, I, P}}},
{"finch_panic_index", {V, {P, I, I, I}}},
{"finch_alloc", {P, {I}}},
{"finch_zalloc", {P, {I}}},
{"finch_str_copy", {V, {P, P}}},
{"finch_str_own", {V, {P}}},
{"finch_str_drop", {V, {P}}},
{"finch_str_from_c", {V, {P, P}}},
{"finch_str_concat", {V, {P, P, P}}},
{"finch_str_eq", {I32, {P, P}}},
{"finch_str_cmp", {I, {P, P}}},
{"finch_str_from_int", {V, {P, I}}},
{"finch_str_from_uint", {V, {P, I}}},
{"finch_str_from_float", {V, {P, D}}},
{"finch_str_from_char", {V, {P, b.getInt8Ty()}}},
{"finch_str_from_bool", {V, {P, I32}}},
{"finch_str_to_int", {I, {P, P, I}}},
{"finch_str_to_float", {D, {P, P, I}}},
{"finch_str_sub", {V, {P, P, I, I, P, I}}},
{"finch_str_find", {I, {P, P}}},
{"finch_str_starts", {I32, {P, P}}},
{"finch_str_ends", {I32, {P, P}}},
{"finch_str_trim", {V, {P, P}}},
{"finch_str_upper", {V, {P, P}}},
{"finch_str_lower", {V, {P, P}}},
{"finch_str_replace", {V, {P, P, P, P}}},
{"finch_str_split", {V, {P, P, P}}},
{"finch_str_join", {V, {P, P, P}}},
{"finch_str_repeat", {V, {P, P, I}}},
{"finch_str_from_bytes", {V, {P, P}}},
{"finch_arr_reserve", {V, {P, I, I}}},
{"finch_arr_make", {V, {P, I, I}}},
{"finch_arr_resize", {V, {P, I, I}}},
{"finch_arr_clone_raw", {V, {P, P, I}}},
{"finch_arr_free", {V, {P}}},
{"finch_arr_insert_gap", {V, {P, I, I, P, I}}},
{"finch_arr_remove_gap", {V, {P, I, I}}},
{"finch_bytes_from_str", {V, {P, P}}},
{"finch_args", {V, {P, I32, P}}},
{"finch_input", {V, {P, P}}},
{"finch_file_exists", {I32, {P}}},
{"finch_read_file", {V, {P, P, P, I}}},
{"finch_write_file", {I32, {P, P}}},
};
auto it = sigs.find(name);
if (it == sigs.end()) {
std::fprintf(stderr, "internal compiler error: unknown runtime function %s\n", name.c_str());
std::exit(2);
}
Function *f = Function::Create(FunctionType::get(it->second.ret, it->second.params, false), Function::ExternalLinkage,
name, mod.get());
if (name == "finch_panic" || name == "finch_panic_index") {
f->addFnAttr(Attribute::NoReturn);
f->addFnAttr(Attribute::Cold);
}
if (name == "finch_str_from_char") f->addParamAttr(1, Attribute::SExt);
return f;
}
// ---------- runtime errors ----------
Value *Codegen::fileName(Pos p) {
std::string key = "file." + std::to_string(p.file);
if (GlobalVariable *g = mod->getNamedGlobal("finch." + key)) return g;
GlobalVariable *g = b.CreateGlobalString(g_files[p.file].path, "finch." + key);
return g;
}
void Codegen::panicIf(Value *cond, const std::string &msg, Pos p) {
BasicBlock *bad = newBlock("panic");
BasicBlock *ok = newBlock("ok");
b.CreateCondBr(cond, bad, ok);
b.SetInsertPoint(bad);
b.CreateCall(rt("finch_panic"), {fileName(p), b.getInt64(p.line), b.CreateGlobalString(msg, "panic.msg")});
b.CreateUnreachable();
b.SetInsertPoint(ok);
}
void Codegen::boundsCheck(Value *i, Value *len, Pos p) {
BasicBlock *bad = newBlock("out.of.range");
BasicBlock *ok = newBlock("in.range");
b.CreateCondBr(b.CreateICmpUGE(i, len), bad, ok); // unsigned: catches negative indexes too
b.SetInsertPoint(bad);
b.CreateCall(rt("finch_panic_index"), {fileName(p), b.getInt64(p.line), i, len});
b.CreateUnreachable();
b.SetInsertPoint(ok);
}
// Dividing by zero is undefined in C; in Finch it is a clear error.
void Codegen::checkDivisor(const Value_ &l, const Value_ &r, Pos p) {
auto *c = dyn_cast<ConstantInt>(r.v);
if (c && c->isZero()) failAt(p.file, p.line, p.col, "division by zero");
if (!c) panicIf(b.CreateICmpEQ(r.v, ConstantInt::get(r.v->getType(), 0)), "division by zero", p);
// the smallest signed number divided by -1 doesn't fit anywhere
if (l.type.isSigned() && (!c || c->isMinusOne())) {
Value *min = ConstantInt::get(l.v->getType(), APInt::getSignedMinValue(l.type.bits()));
Value *minusOne = ConstantInt::get(r.v->getType(), -1, true);
panicIf(b.CreateAnd(b.CreateICmpEQ(l.v, min), b.CreateICmpEQ(r.v, minusOne)),
"division overflows (the smallest number divided by -1)", p);
}
}
Value *Codegen::cGlobal(const std::string &name) {
if (GlobalVariable *gv = mod->getNamedGlobal(name)) return gv;
FType t = resolve(cimports.globals.at(name).type, Pos{});
return new GlobalVariable(*mod, ty(t), false, GlobalValue::ExternalLinkage, nullptr, name);
}
// ---------- calls ----------
Value_ Codegen::call(const CallExpr &c) {
const std::string &n = c.callee;
Pos p = c.pos;
auto one = [&](const char *what) -> const Expr & {
checkArgs(c.args, c.argNames, 1, p, n);
(void)what;
return *c.args[0];
};
if (n == "print") {
checkArgs(c.args, c.argNames, 0, p, n, true);
return print(c.args, p);
}
if (n == "addr") {
checkArgs(c.args, c.argNames, 1, p, n);
return addrOf(c.args, p);
}
if (n == "input") {
if (c.args.size() > 1) checkArgs(c.args, c.argNames, 1, p, n);
Value_ prompt = c.args.empty() ? Value_{strConst(""), FType::Str, false, true}
: coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the question");
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_input"), {out, tmp(prompt.v)});
release(prompt);
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
if (n == "new") {
const Expr &a = one("value");
Value_ v = expr(a);
if (v.type.kind == FType::Void || v.type.kind == FType::Null)
failAt(a.pos.file, a.pos.line, a.pos.col, "new(...) needs a value to put in memory, like new(Point(x: 1, y: 2))");
Value *mem = b.CreateCall(rt("finch_alloc"), {elemSize(v.type)});
b.CreateStore(own(v), mem);
return {mem, FType::ptrTo(v.type)};
}
if (n == "free") {
const Expr &a = one("pointer");
Value_ v = expr(a);
if (!v.type.isPtr())
failAt(a.pos.file, a.pos.line, a.pos.col, "free(...) is only for pointers from new(...) or C; arrays, str and structs are freed by Finch at the end of their block");
if (v.type.elem && owning(*v.type.elem)) {
BasicBlock *drop = newBlock("free.drop"), *done = newBlock("free.done");
b.CreateCondBr(b.CreateIsNull(v.v), done, drop);
b.SetInsertPoint(drop);
dropAt(v.v, *v.type.elem);
b.CreateBr(done);
b.SetInsertPoint(done);
}
b.CreateCall(libc("free", b.getVoidTy(), {b.getPtrTy()}), {v.v});
return {nullptr, FType::Void};
}
if (n == "exit") {
Value *code = coerce(expr(one("code")), FType::I32, c.args[0]->pos, "the exit code").v;
b.CreateCall(libc("fflush", b.getInt32Ty(), {b.getPtrTy()}), {ConstantPointerNull::get(b.getPtrTy())});
b.CreateCall(libc("exit", b.getVoidTy(), {b.getInt32Ty()}), {code});
return {nullptr, FType::Void};
}
if (n == "read_file" || n == "file_exists") {
Value_ path = coerce(expr(one("path")), FType::Str, c.args[0]->pos, "the file name");
Value *pp = tmp(path.v);
Value_ res;
if (n == "file_exists") {
res = {b.CreateICmpNE(b.CreateCall(rt("finch_file_exists"), {pp}), b.getInt32(0)), FType::Bool};
} else {
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_read_file"), {out, pp, fileName(p), b.getInt64(p.line)});
res = {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
release(path);
return res;
}
if (n == "write_file") {
checkArgs(c.args, c.argNames, 2, p, n);
Value_ path = coerce(expr(*c.args[0]), FType::Str, c.args[0]->pos, "the file name");
Value_ text = coerce(expr(*c.args[1]), FType::Str, c.args[1]->pos, "the text");
Value *ok = b.CreateCall(rt("finch_write_file"), {tmp(path.v), tmp(text.v)});
release(path);
release(text);
return {b.CreateICmpNE(ok, b.getInt32(0)), FType::Bool};
}
FType target;
if (typeFromName(n, target)) {
checkArgs(c.args, c.argNames, 1, p, n);
return convert(target, c.args, p, n);
}
ModuleScope &m = modules[curModule];
if (auto it = m.fns.find(n); it != m.fns.end()) return callFinch(it->second, c.args, c.argNames, p, n);
if (StructInfo *s = findStruct("", n, false)) return construct(s, c.args, c.argNames, p);
if (auto cf = cimports.fns.find(n); cf != cimports.fns.end()) {
checkArgs(c.args, c.argNames, cf->second.params.size(), p, n, cf->second.variadic);
return callC(cf->second, c.args, p);
}
if (lookup(n)) failAt(p.file, p.line, p.col, "'" + n + "' is a variable, not a function");
for (auto &[mname, ms] : modules)
if (ms.fns.count(n) && mname != curModule && !mname.empty())
failAt(p.file, p.line, p.col, "there is no function named '" + n + "' here; it is in the module " + mname +
" (write " + mname + "." + n + "(...)" + (m.imports.count(mname) ? "" : " and add: import " + mname) + ")");
failAt(p.file, p.line, p.col, "there is no function named '" + n + "'" + headerHint(n));
}
Value_ Codegen::callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
const std::string &shown) {
const FnDecl &d = *fn.decl;
if (fn.module.empty() && d.name == "main") failAt(p.file, p.line, p.col, "main can't be called, it is where the program starts");
checkArgs(args, names, fn.params.size(), p, shown);
std::vector<Value *> vals;
std::vector<Value_> fresh;
for (size_t i = 0; i < args.size(); i++) {
Value_ v = coerce(exprWant(*args[i], fn.params[i]), fn.params[i], args[i]->pos, "argument '" + d.params[i].name + "'");
vals.push_back(v.v); // owning values are lent: the function never drops them
if (v.fresh) fresh.push_back(v);
}
setLoc(p);
Value *r = b.CreateCall(fn.llvm, vals);
for (auto &v : fresh) release(v);
if (fn.ret.kind == FType::Void) return {nullptr, FType::Void};
return {r, fn.ret, false, owning(fn.ret)};
}
Value_ Codegen::construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p) {
resolveStruct(s);
FType t(FType::Struct);
t.info = s;
t.name = s->name;
t.module = s->module == "C" ? "" : s->module;
std::vector<const StructInfo::F *> fields;
for (auto &f : s->fields) fields.push_back(&f);
bool named = !names.empty() && !names[0].empty();
std::vector<const Expr *> given(fields.size(), nullptr);
for (size_t i = 0; i < args.size(); i++) {
Pos ap = args[i]->pos;
if (names[i].empty() == named)
failAt(ap.file, ap.line, ap.col, "give either all fields by name (x: 1, y: 2) or none (1, 2), not a mix");
if (!named) {
if (args.size() != fields.size())
failAt(p.file, p.line, p.col, s->name + " has " + std::to_string(fields.size()) + " fields, but got " +
std::to_string(args.size()) + " values (or name them: " + s->name + "(field: value))");
given[i] = args[i].get();
continue;
}
size_t k = 0;
while (k < fields.size() && fields[k]->name != names[i]) k++;
if (k == fields.size()) failAt(ap.file, ap.line, ap.col, s->name + " has no field '" + names[i] + "'");
if (given[k]) failAt(ap.file, ap.line, ap.col, "the field '" + names[i] + "' is given twice");
given[k] = args[i].get();
}
Value *v = zero(t);
for (size_t k = 0; k < fields.size(); k++) {
const StructInfo::F &f = *fields[k];
Value_ fv;
if (given[k]) {
fv = coerce(exprWant(*given[k], f.type), f.type, given[k]->pos, "the field '" + f.name + "'");
} else if (f.init) {
// a default value is evaluated where the struct is defined, without the caller's variables
std::vector<Scope> saved;
std::swap(saved, scopes);
scopes.reserve(256);
pushScope();
std::string savedModule = curModule;
curModule = s->module;
fv = coerce(exprWant(*f.init, f.type), f.type, f.init->pos, "the default of '" + f.name + "'");
curModule = savedModule;
scopes.pop_back();
std::swap(saved, scopes);
} else {
continue; // zero
}
v = b.CreateInsertValue(v, own(fv), f.llvmIndex);
}
return {v, t, false, owning(t)};
}
Value_ Codegen::callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p) {
if (!f.unsupported.empty())
failAt(p.file, p.line, p.col, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported);
std::vector<Value_> vals;
for (size_t i = 0; i < args.size(); i++) {
if (i < f.params.size()) {
FType pt = resolve(f.params[i], p);
vals.push_back(coerce(exprWant(*args[i], pt), pt, args[i]->pos, "argument " + std::to_string(i + 1) + " of " + f.name));
} else {
Value_ v = expr(*args[i]);
if (v.type.kind == FType::Void) failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "this gives back nothing");
if (v.type.kind == FType::Struct || v.type.kind == FType::Array)
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "structs and arrays can't be passed to the '...' part of a C function");
vals.push_back(v);
}
}
setLoc(p);
Value_ r = emitCCall(f, declareC(f), vals, p);
for (auto &v : vals) release(v);
return r;
}
// ---------- built-ins ----------
// print(a, b, c) prints the values separated by spaces, then a newline.
Value_ Codegen::print(const std::vector<ExprPtr> &args, Pos) {
for (size_t i = 0; i < args.size(); i++) {
Value_ v = expr(*args[i]);
if (v.type.kind == FType::Void)
failAt(args[i]->pos.file, args[i]->pos.line, args[i]->pos.col, "this gives back nothing, so there is nothing to print");
if (i) printf_(" ", {});
emitPrint(v.v, v.type, false);
release(v);
}
printf_("\n", {});
return {nullptr, FType::Void};
}
// addr(x) gives a pointer to x; addr(function) gives a C-callable function pointer.
Value_ Codegen::addrOf(const std::vector<ExprPtr> &args, Pos p) {
const Expr &e = *args[0];
if (e.kind == ExprKind::Var) {
const std::string &n = static_cast<const VarExpr &>(e).name;
Var *v = lookup(n);
if (!v) {
auto &fns = modules[curModule].fns;
if (auto it = fns.find(n); it != fns.end()) return {cThunk(it->second, p), FType::Ptr};
}
if (v && v->readonly) failAt(e.pos.file, e.pos.line, e.pos.col, "can't take the address of a loop variable");
}
if (e.kind != ExprKind::Var && e.kind != ExprKind::Member && e.kind != ExprKind::Index)
failAt(e.pos.file, e.pos.line, e.pos.col, "addr(...) needs a variable, a field or an element, like addr(x)");
Place pl = place(e, true);
return {pl.addr, FType::ptrTo(pl.type)};
}
// Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), str(x), ptr(x) ...
Value_ Codegen::convert(const FType &to, const std::vector<ExprPtr> &args, Pos, const std::string &name) {
Value_ v = expr(*args[0]);
const FType &from = v.type;
Pos p = args[0]->pos;
if (to.kind == FType::Str) {
Value *out = tmpOf(ty(FType::Str));
if (from.kind == FType::Str) return {copyValue(v.v, FType::Str), FType::Str, false, true};
if (from.isPtr()) return strFromC(v.v);
if (from.isSigned()) b.CreateCall(rt("finch_str_from_int"), {out, b.CreateSExt(v.v, b.getInt64Ty())});
else if (from.isUnsigned()) b.CreateCall(rt("finch_str_from_uint"), {out, b.CreateZExt(v.v, b.getInt64Ty())});
else if (from.isFloat()) b.CreateCall(rt("finch_str_from_float"), {out, b.CreateFPExt(v.v, b.getDoubleTy())});
else if (from.kind == FType::Char) b.CreateCall(rt("finch_str_from_char"), {out, v.v});
else if (from.kind == FType::Bool) b.CreateCall(rt("finch_str_from_bool"), {out, b.CreateZExt(v.v, b.getInt32Ty())});
else if (from.kind == FType::Array && (from.elem->kind == FType::U8 || from.elem->kind == FType::Char)) {
b.CreateCall(rt("finch_str_from_bytes"), {out, tmp(v.v)});
release(v);
} else {
failAt(p.file, p.line, p.col, "can't turn " + from.show() + " into str");
}
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
if (from == to) return v;
if (from.kind == FType::Str && (to.isInt() || to.isFloat())) {
Value *sp = tmp(v.v);
Value_ r;
if (to.isInt()) r = {intCast(b.CreateCall(rt("finch_str_to_int"), {sp, fileName(p), b.getInt64(p.line)}), FType::I64, to), to};
else r = {b.CreateFPCast(b.CreateCall(rt("finch_str_to_float"), {sp, fileName(p), b.getInt64(p.line)}), ty(to)), to};
release(v);
return r;
}
if (to.isInt() || to.kind == FType::Char) {
if (from.isInt() || from.kind == FType::Char) return {b.CreateIntCast(v.v, ty(to), from.isSigned()), to};
if (from.kind == FType::Bool) return {b.CreateZExt(v.v, ty(to)), to};
if (from.isFloat() && to.isInt()) return {to.isSigned() ? b.CreateFPToSI(v.v, ty(to)) : b.CreateFPToUI(v.v, ty(to)), to};
}
if (to.isFloat()) {
if (from.isInt()) return {intToFloat(v.v, from, to), to};
if (from.isFloat()) return {b.CreateFPCast(v.v, ty(to)), to};
}
if (to.kind == FType::Bool && from.isInt()) return {b.CreateICmpNE(v.v, ConstantInt::get(ty(from), 0)), to};
if (to.isPtr() && (from.isPtr() || from.kind == FType::Null)) return {v.v, to};
if (to.isPtr() && from.kind == FType::Str) failAt(p.file, p.line, p.col, "use .ptr to get a str's address (it is only valid while the str lives)");
failAt(p.file, p.line, p.col, "can't turn " + from.show() + " into " + name);
}
// ---------- methods ----------
Value_ Codegen::method(const MethodExpr &m) {
Pos p = m.pos;
// math.sqrt(x), math.Vec(1, 2)
if (m.obj->kind == ExprKind::Var) {
const std::string &mn = static_cast<const VarExpr &>(*m.obj).name;
if (!lookup(mn) && modules[curModule].imports.count(mn)) {
auto mi = modules.find(mn);
if (mi == modules.end()) failAt(p.file, p.line, p.col, "the module '" + mn + "' was not found");
if (auto it = mi->second.fns.find(m.name); it != mi->second.fns.end())
return callFinch(it->second, m.args, m.argNames, p, mn + "." + m.name);
if (auto it = mi->second.structs.find(m.name); it != mi->second.structs.end())
return construct(it->second, m.args, m.argNames, p);
failAt(p.file, p.line, p.col, "the module '" + mn + "' has no function or struct named '" + m.name + "'");
}
if (!lookup(mn) && !cimports.globals.count(mn) && !cimports.consts.count(mn))
failAt(m.obj->pos.file, m.obj->pos.line, m.obj->pos.col,
"there is no variable or module named '" + mn + "' (to use a module, add import " + mn + " at the top)");
}
static const std::set<std::string> changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"};
LRef r = ref(*m.obj, changing.count(m.name) > 0);
FType t = r.isPlace ? r.pl.type : r.val.type;
if (t.kind == FType::Array) {
Value_ recv = r.isPlace ? Value_{r.pl.addr, t} : r.val;
if (!r.isPlace) {
if (changing.count(m.name)) failAt(p.file, p.line, p.col, m.name + "() would change a temporary array; store it in a variable first");
recv = {tmp(r.val.v), t, false, r.val.fresh};
}
Value_ out = arrayMethod(m, &recv);
if (!r.isPlace && r.val.fresh) dropAt(recv.v, t);
return out;
}
if (t.kind == FType::Str) {
Value_ sv = r.isPlace ? Value_{b.CreateLoad(ty(t), r.pl.addr), t} : r.val;
Value *addr = tmp(sv.v); // the runtime takes strings by address
auto argStr = [&](size_t i, const char *what) {
return coerce(expr(*m.args[i]), FType::Str, m.args[i]->pos, what);
};
auto argInt = [&](size_t i, const char *what) {
return coerce(expr(*m.args[i]), FType::I64, m.args[i]->pos, what).v;
};
auto need = [&](size_t n) { checkArgs(m.args, m.argNames, n, p, m.name); };
Value *out = tmpOf(ty(FType::Str));
Value_ res;
const std::string &n = m.name;
auto strRes = [&]() { return Value_{b.CreateLoad(ty(FType::Str), out), FType::Str, false, true}; };
if (n == "sub") {
need(2);
Value *a = argInt(0, "the start"), *e = argInt(1, "the end");
b.CreateCall(rt("finch_str_sub"), {out, addr, a, e, fileName(p), b.getInt64(p.line)});
res = strRes();
} else if (n == "find" || n == "contains" || n == "starts_with" || n == "ends_with") {
need(1);
Value_ t2 = argStr(0, "the text to look for");
Value *tp = tmp(t2.v);
if (n == "find") res = {b.CreateCall(rt("finch_str_find"), {addr, tp}), FType::I64};
else if (n == "contains") res = {b.CreateICmpSGE(b.CreateCall(rt("finch_str_find"), {addr, tp}), b.getInt64(0)), FType::Bool};
else res = {b.CreateICmpNE(b.CreateCall(rt(n == "starts_with" ? "finch_str_starts" : "finch_str_ends"), {addr, tp}), b.getInt32(0)), FType::Bool};
release(t2);
} else if (n == "trim" || n == "upper" || n == "lower") {
need(0);
b.CreateCall(rt("finch_str_" + n), {out, addr});
res = strRes();
} else if (n == "replace") {
need(2);
Value_ a = argStr(0, "the text to replace"), c = argStr(1, "the new text");
b.CreateCall(rt("finch_str_replace"), {out, addr, tmp(a.v), tmp(c.v)});
release(a);
release(c);
res = strRes();
} else if (n == "split") {
need(1);
Value_ sep = argStr(0, "the separator");
FType at = FType::arrayOf(FType::Str);
Value *ao = tmpOf(ty(at));
b.CreateCall(rt("finch_str_split"), {ao, addr, tmp(sep.v)});
release(sep);
res = {b.CreateLoad(ty(at), ao), at, false, true};
} else if (n == "repeat") {
need(1);
b.CreateCall(rt("finch_str_repeat"), {out, addr, argInt(0, "the count")});
res = strRes();
} else if (n == "bytes") {
need(0);
FType at = FType::arrayOf(FType::U8);
Value *ao = tmpOf(ty(at));
b.CreateCall(rt("finch_bytes_from_str"), {ao, addr});
res = {b.CreateLoad(ty(at), ao), at, false, true};
} else {
failAt(p.file, p.line, p.col, "a str has no method '" + n + "' (it has: sub, find, contains, starts_with, ends_with, split, trim, upper, lower, replace, repeat, bytes)");
}
if (!r.isPlace) release(sv);
return res;
}
if (t.kind == FType::Struct)
failAt(p.file, p.line, p.col, "structs don't have methods; write a function and call it: " + m.name + "(value, ...)");
failAt(p.file, p.line, p.col, t.show() + " has no methods");
}
// An element compare for find/contains/sort: -1, 0, 1 (only numbers, chars, bools and str)
static bool comparable(const FType &t) {
return t.isNumber() || t.kind == FType::Char || t.kind == FType::Bool || t.kind == FType::Str;
}
Value_ Codegen::arrayMethod(const MethodExpr &m, Value_ *recv) {
Pos p = m.pos;
const std::string &n = m.name;
FType at = recv->type, et = *at.elem;
llvm::Type *AT = ty(at), *ET = ty(et);
Value *a = recv->v; // the array's address
auto need = [&](size_t k) { checkArgs(m.args, m.argNames, k, p, n); };
auto lenAddr = [&]() { return b.CreateStructGEP(AT, a, 1); };
auto len = [&]() { return b.CreateLoad(b.getInt64Ty(), lenAddr()); };
auto data = [&]() { return b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(AT, a, 0)); };
auto elemValue = [&](size_t i) { return own(coerce(exprWant(*m.args[i], et), et, m.args[i]->pos, "the element")); };
if (n == "push") {
need(1);
Value *v = elemValue(0);
Value *l = len();
b.CreateCall(rt("finch_arr_reserve"), {a, b.CreateAdd(l, b.getInt64(1)), elemSize(et)});
b.CreateStore(v, b.CreateInBoundsGEP(ET, data(), l));
b.CreateStore(b.CreateAdd(l, b.getInt64(1)), lenAddr());
return {nullptr, FType::Void};
}
if (n == "pop") {
need(0);
Value *l = len();
panicIf(b.CreateICmpEQ(l, b.getInt64(0)), "pop() on an empty array", p);
Value *last = b.CreateSub(l, b.getInt64(1));
Value *v = b.CreateLoad(ET, b.CreateInBoundsGEP(ET, data(), last));
b.CreateStore(last, lenAddr());
return {v, et, false, owning(et)};
}
if (n == "insert") {
need(2);
Value *i = coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the position").v;
Value *v = elemValue(1);
b.CreateCall(rt("finch_arr_insert_gap"), {a, i, elemSize(et), fileName(p), b.getInt64(p.line)});
b.CreateStore(v, b.CreateInBoundsGEP(ET, data(), i));
return {nullptr, FType::Void};
}
if (n == "remove") {
need(1);
Value *i = coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the position").v;
boundsCheck(i, len(), p);
Value *v = b.CreateLoad(ET, b.CreateInBoundsGEP(ET, data(), i));
b.CreateCall(rt("finch_arr_remove_gap"), {a, i, elemSize(et)});
return {v, et, false, owning(et)};
}
if (n == "clear" || n == "resize") {
need(n == "clear" ? 0 : 1);
Value *nl = n == "clear" ? (Value *)b.getInt64(0) : coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the new length").v;
if (owning(et)) { // drop the elements that go away
Value *l = len(), *d = data();
Value *from = b.CreateSelect(b.CreateICmpSLT(nl, l), b.CreateSelect(b.CreateICmpSLT(nl, b.getInt64(0)), b.getInt64(0), nl), l);
forN(b.CreateSub(l, from), [&](Value *k) { dropAt(b.CreateInBoundsGEP(ET, d, b.CreateAdd(from, k)), et); });
}
b.CreateCall(rt("finch_arr_resize"), {a, nl, elemSize(et)});
return {nullptr, FType::Void};
}
if (n == "find" || n == "contains") {
need(1);
if (!comparable(et)) failAt(p.file, p.line, p.col, n + "() works on arrays of numbers, chars, bools or str");
Value_ x = coerce(exprWant(*m.args[0], et), et, m.args[0]->pos, "the value to look for");
Value *xv = x.v;
Value *res = tmpOf(b.getInt64Ty());
b.CreateStore(b.getInt64(-1), res);
Value *l = len(), *d = data();
Value *i = tmpOf(b.getInt64Ty());
b.CreateStore(b.getInt64(0), i);
BasicBlock *cond = newBlock("find.cond"), *body = newBlock("find.body"), *hit = newBlock("find.hit"),
*next = newBlock("find.next"), *end = newBlock("find.end");
b.CreateBr(cond);
b.SetInsertPoint(cond);
Value *iv = b.CreateLoad(b.getInt64Ty(), i);
b.CreateCondBr(b.CreateICmpSLT(iv, l), body, end);
b.SetInsertPoint(body);
Value_ ev{b.CreateLoad(ET, b.CreateInBoundsGEP(ET, d, iv)), et};
Value *eq = compare(BinOp::Eq, ev, {xv, et}, p).v;
b.CreateCondBr(eq, hit, next);
b.SetInsertPoint(hit);
b.CreateStore(iv, res);
b.CreateBr(end);
b.SetInsertPoint(next);
b.CreateStore(b.CreateAdd(iv, b.getInt64(1)), i);
b.CreateBr(cond);
b.SetInsertPoint(end);
release(x);
Value *r = b.CreateLoad(b.getInt64Ty(), res);
if (n == "find") return {r, FType::I64};
return {b.CreateICmpSGE(r, b.getInt64(0)), FType::Bool};
}
if (n == "join") {
need(1);
if (et.kind != FType::Str) failAt(p.file, p.line, p.col, "join() works on an array of str");
Value_ sep = coerce(expr(*m.args[0]), FType::Str, m.args[0]->pos, "the separator");
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_join"), {out, a, tmp(sep.v)});
release(sep);
return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true};
}
if (n == "slice") {
need(2);
Value *s = coerce(expr(*m.args[0]), FType::I64, m.args[0]->pos, "the start").v;
Value *e = coerce(expr(*m.args[1]), FType::I64, m.args[1]->pos, "the end").v;
Value *l = len();
panicIf(b.CreateOr(b.CreateOr(b.CreateICmpSLT(s, b.getInt64(0)), b.CreateICmpSGT(e, l)), b.CreateICmpSGT(s, e)),
"slice(start, end) is out of range", p);
Value *cnt = b.CreateSub(e, s);
Value *out = tmpOf(AT);
b.CreateCall(rt("finch_arr_make"), {out, cnt, elemSize(et)});
Value *src = data();
Value *dst = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(AT, out, 0));
forN(cnt, [&](Value *k) {
copyAt(b.CreateInBoundsGEP(ET, dst, k), b.CreateInBoundsGEP(ET, src, b.CreateAdd(s, k)), et);
});
return {b.CreateLoad(AT, out), at, false, true};
}
if (n == "reverse") {
need(0);
Value *l = len(), *d = data();
forN(b.CreateSDiv(l, b.getInt64(2)), [&](Value *k) {
Value *x = b.CreateInBoundsGEP(ET, d, k);
Value *y = b.CreateInBoundsGEP(ET, d, b.CreateSub(b.CreateSub(l, b.getInt64(1)), k));
Value *xv = b.CreateLoad(ET, x), *yv = b.CreateLoad(ET, y);
b.CreateStore(yv, x);
b.CreateStore(xv, y);
});
return {nullptr, FType::Void};
}
if (n == "sort") {
need(0);
if (!comparable(et) || et.kind == FType::Bool) failAt(p.file, p.line, p.col, "sort() works on arrays of numbers, chars or str");
std::string key = "cmp." + typeKey(et);
Function *cmp = helpers[key];
if (!cmp) {
cmp = Function::Create(FunctionType::get(b.getInt32Ty(), {b.getPtrTy(), b.getPtrTy()}, false),
Function::InternalLinkage, "finch." + key, mod.get());
helpers[key] = cmp;
IRBuilderBase::InsertPointGuard g(b);
DISubprogram *savedFn = diFn;
diFn = nullptr;
b.SetInsertPoint(BasicBlock::Create(ctx, "entry", cmp));
b.SetCurrentDebugLocation(DebugLoc());
Value *x = cmp->getArg(0), *y = cmp->getArg(1);
Value *r;
if (et.kind == FType::Str) {
r = b.CreateTrunc(b.CreateCall(rt("finch_str_cmp"), {x, y}), b.getInt32Ty());
} else {
Value_ xv{b.CreateLoad(ET, x), et}, yv{b.CreateLoad(ET, y), et};
Value *lt = compare(BinOp::Less, xv, yv, p).v, *gt = compare(BinOp::Greater, xv, yv, p).v;
r = b.CreateSub(b.CreateZExt(gt, b.getInt32Ty()), b.CreateZExt(lt, b.getInt32Ty()));
}
b.CreateRet(r);
diFn = savedFn;
}
b.CreateCall(libc("qsort", b.getVoidTy(), {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty(), b.getPtrTy()}),
{data(), len(), elemSize(et), cmp});
return {nullptr, FType::Void};
}
failAt(p.file, p.line, p.col, "an array has no method '" + n + "' (it has: push, pop, insert, remove, clear, resize, find, contains, join, slice, reverse, sort)");
}
} // namespace finch

265
src/codegen_impl.h Normal file
View file

@ -0,0 +1,265 @@
#pragma once
// Internal to the code generator: the Codegen class, shared by codegen*.cpp and abi.cpp.
#include "codegen.h"
#include "error.h"
#include <llvm/IR/Constants.h>
#include <llvm/IR/DIBuilder.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/Verifier.h>
#include <llvm/Support/raw_ostream.h>
#include <functional>
#include <map>
#include <set>
#include <unordered_map>
using FType = ::Type; // Finch types (llvm::Type is the LLVM one)
struct StructInfo {
std::string name;
std::string module; // "" main file, a module name, or "C" for imported C structs
bool isC = false;
const StructDecl *decl = nullptr; // Finch structs
const CStruct *cdecl = nullptr; // C structs
struct F {
std::string name;
FType type;
const Expr *init = nullptr; // default value
bool hidden = false; // C union/bitfield padding: has no name in Finch
unsigned llvmIndex = 0; // C structs carry explicit padding between fields
};
std::vector<F> fields;
llvm::StructType *llvm = nullptr;
int state = 0; // 0 = not resolved, 1 = resolving (cycle check), 2 = done
bool owning = false;
std::string unsupported; // C structs Finch can't lay out
Pos pos;
};
namespace finch {
struct Value_ {
llvm::Value *v;
FType type;
bool literal = false; // a number written in the code (or a C constant): adapts to the type around it
bool fresh = false; // an owning value nobody holds yet: it must be stored (moved) or released
};
struct Var {
llvm::Value *slot;
FType type;
bool readonly = false; // loop variables
std::string readonlyWhy;
bool owned = false; // dropped when its scope ends
int order = 0; // declaration order, for defer
bool moved = false; // returned out of the function: not dropped on that path
};
struct Cleanup {
bool isDefer;
std::string var; // Drop: the variable
const Stmt *body = nullptr; // Defer: the statement
int order = 0; // Defer: variables declared after it are not visible
};
struct Scope {
std::unordered_map<std::string, Var> vars;
std::vector<Cleanup> cleanups;
};
struct Fn {
llvm::Function *llvm = nullptr;
const FnDecl *decl = nullptr;
std::string module;
std::vector<bool> borrowParam; // owning params the function never changes: no copy
llvm::Function *cThunk = nullptr;
FType ret; // resolved types
std::vector<FType> params;
};
struct Loop {
llvm::BasicBlock *continueTo, *breakTo;
size_t scopeDepth; // scopes opened inside the loop are cleaned up by break/continue
};
struct Place { // something that can be assigned to
llvm::Value *addr;
FType type;
};
struct LRef {
bool isPlace;
Place pl;
Value_ val;
};
struct ModuleScope {
std::map<std::string, Fn> fns;
std::map<std::string, StructInfo *> structs;
std::set<std::string> imports; // Finch modules visible here
};
// How a C struct travels through registers (System V x86-64).
struct AbiArg {
enum Kind { Direct, Expand, Memory } kind = Direct;
std::vector<llvm::Type *> parts; // Expand: one value per eightbyte
llvm::Type *coerced = nullptr; // Expand: { parts... } for returns
};
class Codegen {
public:
Codegen(llvm::LLVMContext &c, const std::vector<Program> &progs, const CImports &ci, llvm::TargetMachine &tm,
bool debug);
std::unique_ptr<llvm::Module> run();
// ---------- state ----------
llvm::LLVMContext &ctx;
llvm::IRBuilder<> b;
std::unique_ptr<llvm::Module> mod;
const std::vector<Program> &progs;
const CImports &cimports;
const llvm::DataLayout *dl;
std::map<std::string, ModuleScope> modules;
std::map<std::string, StructInfo *> cStructs;
std::vector<std::unique_ptr<StructInfo>> structStore;
std::vector<Scope> scopes;
std::vector<Loop> loops;
const Fn *curFn = nullptr;
std::string curModule;
int varOrder = 0;
int deferLimit = -1; // while emitting a defer: hide variables declared after it
int inDefer = 0;
std::map<std::string, llvm::Function *> helpers; // drop/copy/print per type
// debug info (-g)
bool debug;
std::unique_ptr<llvm::DIBuilder> di;
std::vector<llvm::DIFile *> diFiles;
llvm::DICompileUnit *diUnit = nullptr;
llvm::DISubprogram *diFn = nullptr;
std::map<std::string, llvm::DIType *> diTypes;
// ---------- codegen.cpp: program, statements, expressions ----------
void declareStructs();
void declareFns();
void declare(const Program &p, const FnDecl &fn);
void define(Fn &fn);
void defineMainWrapper();
void block(const BlockStmt &blk);
void blockBody(const BlockStmt &blk);
void stmt(const Stmt &s);
void varDecl(const VarDeclStmt &s);
void assign(const AssignStmt &s);
void ifStmt(const IfStmt &s);
void whileStmt(const WhileStmt &s);
void forStmt(const ForStmt &s);
void forEachStmt(const ForEachStmt &s);
void returnStmt(const ReturnStmt &s);
void jump(bool isBreak, Pos p);
llvm::Value *condition(const Expr &e);
void continueAt(llvm::BasicBlock *bb, llvm::BasicBlock *deadEnd);
bool terminated();
llvm::BasicBlock *newBlock(const char *name);
void pushScope();
void popScope(); // emits the scope's cleanups if reachable
void emitCleanups(size_t downTo); // run cleanups of scopes [downTo, end) without popping
void emitScopeCleanups(size_t idx);
llvm::AllocaInst *slot(const FType &t, const std::string &name);
Var *lookup(const std::string &name);
Var &addVar(Pos p, const std::string &name, const FType &t, llvm::Value *init, bool owned, bool readonly = false,
unsigned argNo = 0);
Value_ expr(const Expr &e);
Value_ exprWant(const Expr &e, const FType &want);
Value_ binary(const BinaryExpr &e);
Value_ unary(const UnaryExpr &e);
Value_ logic(const BinaryExpr &e);
Value_ arith(BinOp op, Value_ l, Value_ r, Pos p);
Value_ arithOp(BinOp op, Value_ l, Value_ r, Pos p);
Value_ compare(BinOp op, Value_ l, Value_ r, Pos p);
bool unify(Value_ &l, Value_ &r);
[[noreturn]] void badOperands(BinOp op, const Value_ &l, const Value_ &r, Pos p);
LRef ref(const Expr &e, bool forWrite = false); // an address if `e` has one, else a value
Place place(const Expr &e, bool forWrite = false);
LRef member(const MemberExpr &m, bool forWrite);
LRef index(const IndexExpr &e, bool forWrite);
Value_ arrayLit(const ArrayLitExpr &a, const FType *want);
std::string rootVar(const Expr &e);
bool mutates(const std::string &name, const Stmt &s);
bool mutatesExpr(const std::string &name, const Expr &e);
// ---------- codegen_types.cpp: types, conversions, ownership, printing ----------
llvm::Type *ty(const FType &t);
FType resolve(const FType &t, Pos p);
FType resolveT(const FType &t, Pos p, bool deep); // deep: structs by value must be complete
void forN(llvm::Value *n, const std::function<void(llvm::Value *)> &body); // for i in 0..n
llvm::Value *tmpOf(llvm::Type *t); // an uninitialised stack slot
void resolveStruct(StructInfo *s);
StructInfo *findStruct(const std::string &module, const std::string &name, bool qualified);
bool owning(const FType &t);
llvm::Constant *zero(const FType &t);
Value_ defaultValue(const FType &t, Pos p);
static bool widens(const FType &from, const FType &to);
static bool fits(llvm::ConstantInt *c, const FType &from, const FType &to);
llvm::Value *intCast(llvm::Value *v, const FType &from, const FType &to);
llvm::Value *intToFloat(llvm::Value *v, const FType &from, const FType &to);
Value_ coerce(Value_ v, const FType &want, Pos p, const std::string &what);
llvm::Value *own(const Value_ &v); // something that can be stored: moved if fresh, else copied
void release(const Value_ &v); // drop a fresh value nobody kept
llvm::Value *copyValue(llvm::Value *v, const FType &t);
void dropValue(llvm::Value *v, const FType &t);
void dropAt(llvm::Value *addr, const FType &t);
void copyAt(llvm::Value *dst, llvm::Value *src, const FType &t);
llvm::Function *dropFn(const FType &t);
llvm::Function *copyFn(const FType &t);
std::string typeKey(const FType &t);
llvm::Value *tmp(llvm::Value *v); // a stack copy of v, to pass by address
void emitPrint(llvm::Value *v, const FType &t, bool quoted);
llvm::Function *printFn(const FType &t);
void printf_(const std::string &fmt, std::vector<llvm::Value *> args);
llvm::Value *strConst(const std::string &s);
llvm::Value *cstr(llvm::Value *strValue); // str -> char* for C
Value_ strFromC(llvm::Value *p); // char* -> borrowed str
llvm::Value *elemSize(const FType &t);
// ---------- codegen_builtins.cpp: calls, built-ins, methods ----------
Value_ call(const CallExpr &c);
Value_ method(const MethodExpr &m);
Value_ callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
const std::string &shownName);
Value_ construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p);
Value_ callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p);
Value_ convert(const FType &to, const std::vector<ExprPtr> &args, Pos p, const std::string &name);
Value_ print(const std::vector<ExprPtr> &args, Pos p);
Value_ addrOf(const std::vector<ExprPtr> &args, Pos p);
Value_ arrayMethod(const MethodExpr &m, Value_ *recv);
bool isBuiltin(const std::string &n);
std::string headerHint(const std::string &name);
void checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p,
const std::string &name, bool variadic = false);
llvm::Value *cGlobal(const std::string &name);
llvm::Function *rt(const std::string &name); // a runtime function by name
// runtime errors
llvm::Value *fileName(Pos p);
void panicIf(llvm::Value *cond, const std::string &msg, Pos p);
void boundsCheck(llvm::Value *i, llvm::Value *len, Pos p);
void checkDivisor(const Value_ &l, const Value_ &r, Pos p);
llvm::Function *libc(const char *name, llvm::Type *ret, std::vector<llvm::Type *> params, bool vararg = false);
// ---------- abi.cpp: C calling convention for structs by value ----------
AbiArg classify(const FType &t);
llvm::Function *declareC(const CFunc &f);
Value_ emitCCall(const CFunc &f, llvm::Function *fn, std::vector<Value_> args, Pos p);
llvm::Function *cThunk(Fn &fn, Pos p);
// ---------- debug info ----------
void setLoc(Pos p);
llvm::DIType *diType(const FType &t);
};
} // namespace finch

500
src/codegen_types.cpp Normal file
View file

@ -0,0 +1,500 @@
// Code generation, part 2: types, conversions, ownership (copy/drop) and printing.
#include "codegen_impl.h"
using namespace llvm;
namespace finch {
// ---------- types ----------
llvm::Type *Codegen::ty(const FType &t) {
switch (t.kind) {
case FType::Void: return b.getVoidTy();
case FType::Bool: return b.getInt1Ty();
case FType::Char:
case FType::I8:
case FType::U8: return b.getInt8Ty();
case FType::I16:
case FType::U16: return b.getInt16Ty();
case FType::I32:
case FType::U32: return b.getInt32Ty();
case FType::I64:
case FType::U64: return b.getInt64Ty();
case FType::F32: return b.getFloatTy();
case FType::F64: return b.getDoubleTy();
case FType::Str:
case FType::Array: return StructType::get(ctx, {b.getPtrTy(), b.getInt64Ty(), b.getInt64Ty()});
case FType::Ptr:
case FType::Null: return b.getPtrTy();
case FType::Fixed: return ArrayType::get(ty(*t.elem), t.count);
case FType::Struct: return t.info->llvm;
case FType::Named: break;
}
std::fprintf(stderr, "internal compiler error: unresolved type %s\n", t.show().c_str());
std::exit(2);
}
FType Codegen::resolve(const FType &t, Pos p) { return resolveT(t, p, true); }
StructInfo *Codegen::findStruct(const std::string &module, const std::string &name, bool qualified) {
if (qualified) {
if (module == "C") {
auto it = cStructs.find(name);
return it == cStructs.end() ? nullptr : it->second;
}
auto m = modules.find(module);
if (m == modules.end()) return nullptr;
auto it = m->second.structs.find(name);
return it == m->second.structs.end() ? nullptr : it->second;
}
auto &here = modules[curModule].structs;
if (auto it = here.find(name); it != here.end()) return it->second;
if (auto it = cStructs.find(name); it != cStructs.end()) return it->second;
return nullptr;
}
FType Codegen::resolveT(const FType &t, Pos p, bool deep) {
switch (t.kind) {
case FType::Ptr:
if (!t.elem) return t;
return FType::ptrTo(resolveT(*t.elem, p, false));
case FType::Array: return FType::arrayOf(resolveT(*t.elem, p, false));
case FType::Fixed: {
FType f = t;
f.elem = std::make_shared<FType>(resolveT(*t.elem, p, deep));
return f;
}
case FType::Named: {
bool qualified = !t.module.empty();
if (qualified && t.module != "C" && !modules[curModule].imports.count(t.module))
failAt(p.file, p.line, p.col, "'" + t.module + "' is not imported here (add: import " + t.module + ")");
StructInfo *s = findStruct(t.module, t.name, qualified);
if (!s) failAt(p.file, p.line, p.col, "there is no type named '" + t.show() + "'");
FType r(FType::Struct);
r.info = s;
r.name = s->name;
r.module = s->module == "C" ? "" : s->module; // shown as math.Vec; C and main-file structs by name
if (deep) resolveStruct(s);
return r;
}
default: return t;
}
}
void Codegen::resolveStruct(StructInfo *s) {
if (s->state == 2) return;
if (s->state == 1)
failAt(s->pos.file, s->pos.line, s->pos.col,
"the struct " + s->name + " contains itself; use ptr[" + s->name + "] or []" + s->name + " for that field");
s->state = 1;
std::string savedModule = curModule;
curModule = s->isC ? "" : s->module;
std::vector<llvm::Type *> elems;
if (!s->isC) {
for (const Field &f : s->decl->fields) {
StructInfo::F nf;
nf.name = f.name;
nf.type = resolveT(f.type, f.pos, true);
if (nf.type.kind == FType::Void) failAt(f.pos.file, f.pos.line, f.pos.col, "a field can't have the type 'nothing'");
nf.init = f.init.get();
nf.llvmIndex = elems.size();
elems.push_back(ty(nf.type));
s->owning |= owning(nf.type);
s->fields.push_back(nf);
}
s->llvm->setBody(elems);
} else {
// Lay the C struct out byte for byte: explicit padding, packed, exactly C's offsets.
long long off = 0;
Pos none;
for (const CField &cf : s->cdecl->fields) {
if (cf.hidden || cf.offset < off) continue;
FType ft = resolveT(cf.type, none, true);
llvm::Type *lt = ty(ft);
long long size = dl->getTypeAllocSize(lt);
if (size != cf.size) continue; // something Finch can't mirror; the padding covers it
if (cf.offset > off) elems.push_back(ArrayType::get(b.getInt8Ty(), cf.offset - off));
StructInfo::F nf;
nf.name = cf.name;
nf.type = ft;
nf.llvmIndex = elems.size();
elems.push_back(lt);
s->fields.push_back(nf);
off = cf.offset + size;
}
if (s->cdecl->size > off) elems.push_back(ArrayType::get(b.getInt8Ty(), s->cdecl->size - off));
s->llvm->setBody(elems, true);
if (s->cdecl->hasHidden && s->unsupported.empty()) s->unsupported = "has fields Finch can't see (unions or bit fields)";
}
curModule = savedModule;
s->state = 2;
}
bool Codegen::owning(const FType &t) {
switch (t.kind) {
case FType::Str:
case FType::Array: return true;
case FType::Struct:
resolveStruct(t.info);
return t.info->owning;
default: return false;
}
}
Constant *Codegen::zero(const FType &t) { return Constant::getNullValue(ty(t)); }
Value_ Codegen::defaultValue(const FType &t, Pos p) {
if (t.kind == FType::Struct && !t.info->isC) {
for (auto &f : t.info->fields)
if (f.init) return construct(t.info, {}, {}, p);
}
return {zero(t), t, false, owning(t)};
}
// Can every value of `from` be stored in `to`? (i8 -> i32 yes, i32 -> u32 no)
bool Codegen::widens(const FType &from, const FType &to) {
if (!from.isInt() || !to.isInt()) return false;
if (from.isSigned() && to.isUnsigned()) return false;
return to.bits() > from.bits();
}
// Does the constant number `c` (of type `from`) fit into `to`?
bool Codegen::fits(ConstantInt *c, const FType &from, const FType &to) {
if (!to.isInt()) return false;
bool negative = from.isSigned() && c->isNegative();
if (negative && to.isUnsigned()) return false;
if (negative) return c->getSExtValue() >= -(1LL << (to.bits() - 1));
unsigned long long v = c->getZExtValue();
if (to.bits() == 64) return to.isUnsigned() || v <= (unsigned long long)INT64_MAX;
unsigned long long max = to.isUnsigned() ? (1ULL << to.bits()) - 1 : (1ULL << (to.bits() - 1)) - 1;
return v <= max;
}
Value *Codegen::intCast(Value *v, const FType &from, const FType &to) { return b.CreateIntCast(v, ty(to), from.isSigned()); }
Value *Codegen::intToFloat(Value *v, const FType &from, const FType &to) {
return from.isSigned() ? b.CreateSIToFP(v, ty(to)) : b.CreateUIToFP(v, ty(to));
}
// Implicit conversions: only the ones that can't surprise you.
Value_ Codegen::coerce(Value_ v, const FType &want, Pos p, const std::string &what) {
const FType &from = v.type;
if (from == want) return v;
if (from.kind == FType::Null && want.isPtr()) return {ConstantPointerNull::get(b.getPtrTy()), want};
// any pointer fits an untyped `ptr`; an untyped ptr fits any pointer
if (want.isPtr() && from.isPtr() && (!want.elem || !from.elem)) return {v.v, want};
if (from.isInt() && want.isInt()) {
auto *c = v.literal ? dyn_cast<ConstantInt>(v.v) : nullptr;
if ((c && fits(c, from, want)) || widens(from, want)) return {intCast(v.v, from, want), want, (bool)c};
if (c)
failAt(p.file, p.line, p.col, "the number " + std::to_string(from.isSigned() ? c->getSExtValue() : (long long)c->getZExtValue()) +
" doesn't fit in " + want.show());
}
if (from.isInt() && want.isFloat()) return {intToFloat(v.v, from, want), want, v.literal};
if (from.kind == FType::F32 && want.kind == FType::F64) return {b.CreateFPExt(v.v, ty(want)), want};
if (from.kind == FType::F64 && want.kind == FType::F32 && v.literal) return {b.CreateFPTrunc(v.v, ty(want)), want, true};
std::string msg = what + " must be " + want.show() + ", but this is " + from.show();
if (from.isNumber() && want.isNumber())
msg += " (use " + want.show() + "(...) to convert" + (from.isFloat() && want.isInt() ? ", it cuts off the fraction)" : ")");
else if (want.kind == FType::Str && (from.isNumber() || from.kind == FType::Bool || from.kind == FType::Char))
msg += " (turn it into text with str(...))";
else if (want.isPtr() && from.kind == FType::Str)
msg += " (use .ptr to get the text's address)";
if (from.kind == FType::Null) msg = what + " can't be null (only pointers can)";
failAt(p.file, p.line, p.col, msg);
}
// ---------- ownership ----------
Value *Codegen::tmpOf(llvm::Type *t) {
Function *f = b.GetInsertBlock()->getParent();
IRBuilder<> entry(&f->getEntryBlock(), f->getEntryBlock().begin());
AllocaInst *a = entry.CreateAlloca(t);
if (t->isStructTy()) a->setAlignment(Align(16));
return a;
}
Value *Codegen::tmp(Value *v) {
Value *a = tmpOf(v->getType());
b.CreateStore(v, a);
return a;
}
Value *Codegen::own(const Value_ &v) {
if (!owning(v.type) || v.fresh) return v.v;
return copyValue(v.v, v.type);
}
void Codegen::release(const Value_ &v) {
if (v.fresh && v.v && owning(v.type)) dropValue(v.v, v.type);
}
Value *Codegen::copyValue(Value *v, const FType &t) {
if (!owning(t)) return v;
Value *out = tmpOf(ty(t));
copyAt(out, tmp(v), t);
return b.CreateLoad(ty(t), out);
}
void Codegen::dropValue(Value *v, const FType &t) {
if (owning(t)) dropAt(tmp(v), t);
}
void Codegen::dropAt(Value *addr, const FType &t) {
if (!owning(t)) return;
if (t.kind == FType::Str) b.CreateCall(rt("finch_str_drop"), {addr});
else b.CreateCall(dropFn(t), {addr});
}
void Codegen::copyAt(Value *dst, Value *src, const FType &t) {
if (!owning(t)) {
b.CreateStore(b.CreateLoad(ty(t), src), dst);
return;
}
if (t.kind == FType::Str) b.CreateCall(rt("finch_str_copy"), {dst, src});
else b.CreateCall(copyFn(t), {dst, src});
}
void Codegen::forN(Value *n, const std::function<void(Value *)> &body) {
Value *i = tmpOf(b.getInt64Ty());
b.CreateStore(b.getInt64(0), i);
BasicBlock *cond = newBlock("n.cond"), *loop = newBlock("n.body"), *end = newBlock("n.end");
b.CreateBr(cond);
b.SetInsertPoint(cond);
Value *iv = b.CreateLoad(b.getInt64Ty(), i);
b.CreateCondBr(b.CreateICmpSLT(iv, n), loop, end);
b.SetInsertPoint(loop);
body(iv);
b.CreateStore(b.CreateAdd(iv, b.getInt64(1)), i);
b.CreateBr(cond);
b.SetInsertPoint(end);
}
std::string Codegen::typeKey(const FType &t) {
switch (t.kind) {
case FType::Array: return "arr_" + typeKey(*t.elem);
case FType::Fixed: return "fix" + std::to_string(t.count) + "_" + typeKey(*t.elem);
case FType::Ptr: return "ptr";
case FType::Struct: return "S_" + (t.info->module.empty() ? std::string("main") : t.info->module) + "_" + t.info->name;
default: return t.show();
}
}
// Helper functions are generated once per type and shared; they have no source position.
struct HelperScope {
Codegen &cg;
IRBuilderBase::InsertPointGuard guard;
DISubprogram *savedFn;
HelperScope(Codegen &c, Function *f) : cg(c), guard(c.b), savedFn(c.diFn) {
c.diFn = nullptr;
c.b.SetInsertPoint(BasicBlock::Create(c.ctx, "entry", f));
c.b.SetCurrentDebugLocation(DebugLoc());
}
~HelperScope() { cg.diFn = savedFn; }
};
Function *Codegen::dropFn(const FType &t) {
std::string key = "drop." + typeKey(t);
if (auto it = helpers.find(key); it != helpers.end()) return it->second;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::InternalLinkage,
"finch." + key, mod.get());
helpers[key] = f;
HelperScope hs(*this, f);
Value *a = f->getArg(0);
if (t.kind == FType::Array) {
FType et = *t.elem;
if (owning(et)) {
Value *data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0));
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1));
forN(len, [&](Value *i) { dropAt(b.CreateInBoundsGEP(ty(et), data, i), et); });
}
b.CreateCall(rt("finch_arr_free"), {a});
} else if (t.kind == FType::Struct) {
for (auto &fl : t.info->fields)
if (owning(fl.type)) dropAt(b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex), fl.type);
}
b.CreateRetVoid();
return f;
}
Function *Codegen::copyFn(const FType &t) {
std::string key = "copy." + typeKey(t);
if (auto it = helpers.find(key); it != helpers.end()) return it->second;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy(), b.getPtrTy()}, false),
Function::InternalLinkage, "finch." + key, mod.get());
helpers[key] = f;
HelperScope hs(*this, f);
Value *dst = f->getArg(0), *src = f->getArg(1);
if (t.kind == FType::Array) {
FType et = *t.elem;
b.CreateCall(rt("finch_arr_clone_raw"), {dst, src, elemSize(et)});
if (owning(et)) {
Value *d = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), dst, 0));
Value *s = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), src, 0));
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), src, 1));
forN(len, [&](Value *i) {
copyAt(b.CreateInBoundsGEP(ty(et), d, i), b.CreateInBoundsGEP(ty(et), s, i), et);
});
}
} else if (t.kind == FType::Struct) {
b.CreateStore(b.CreateLoad(ty(t), src), dst);
for (auto &fl : t.info->fields)
if (owning(fl.type))
copyAt(b.CreateStructGEP(t.info->llvm, dst, fl.llvmIndex), b.CreateStructGEP(t.info->llvm, src, fl.llvmIndex), fl.type);
}
b.CreateRetVoid();
return f;
}
// ---------- strings ----------
Value *Codegen::strConst(const std::string &s) {
Constant *g = b.CreateGlobalString(s, "str");
return ConstantStruct::get(cast<StructType>(ty(FType::Str)), {g, b.getInt64(s.size()), b.getInt64(0)});
}
// str -> char* for C (an empty str may have no memory at all; C gets "" then)
Value *Codegen::cstr(Value *s) {
Value *p = b.CreateExtractValue(s, 0);
return b.CreateSelect(b.CreateIsNull(p), b.CreateGlobalString("", "empty"), p);
}
Value_ Codegen::strFromC(Value *p) {
Value *out = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_from_c"), {out, p});
return {b.CreateLoad(ty(FType::Str), out), FType::Str};
}
Value *Codegen::elemSize(const FType &t) { return b.getInt64(dl->getTypeAllocSize(ty(t))); }
// ---------- printing ----------
void Codegen::printf_(const std::string &fmt, std::vector<Value *> args) {
args.insert(args.begin(), b.CreateGlobalString(fmt, "fmt"));
b.CreateCall(libc("printf", b.getInt32Ty(), {b.getPtrTy()}, true), args);
}
void Codegen::emitPrint(Value *v, const FType &t, bool quoted) {
if (t.isSigned()) printf_("%lld", {b.CreateSExt(v, b.getInt64Ty())});
else if (t.isUnsigned()) printf_("%llu", {b.CreateZExt(v, b.getInt64Ty())});
else if (t.isFloat()) printf_("%g", {b.CreateFPExt(v, b.getDoubleTy())});
else if (t.kind == FType::Char) printf_(quoted ? "'%c'" : "%c", {b.CreateZExt(v, b.getInt32Ty())});
else if (t.kind == FType::Bool) printf_("%s", {b.CreateSelect(v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false"))});
else if (t.kind == FType::Str)
printf_(quoted ? "\"%.*s\"" : "%.*s", {b.CreateTrunc(b.CreateExtractValue(v, 1), b.getInt32Ty()), cstr(v)});
else if (t.isPtr()) printf_("%p", {v});
else if (t.kind == FType::Null) printf_("null", {});
else b.CreateCall(printFn(t), {tmp(v)});
}
Function *Codegen::printFn(const FType &t) {
std::string key = "print." + typeKey(t);
if (auto it = helpers.find(key); it != helpers.end()) return it->second;
Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::InternalLinkage,
"finch." + key, mod.get());
helpers[key] = f;
HelperScope hs(*this, f);
Value *a = f->getArg(0);
if (t.kind == FType::Array || t.kind == FType::Fixed) {
FType et = *t.elem;
Value *data, *len;
if (t.kind == FType::Fixed) {
data = a;
len = b.getInt64(t.count);
} else {
data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), a, 0));
len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), a, 1));
}
printf_("[", {});
forN(len, [&](Value *i) {
BasicBlock *sep = newBlock("sep"), *item = newBlock("item");
b.CreateCondBr(b.CreateICmpSGT(i, b.getInt64(0)), sep, item);
b.SetInsertPoint(sep);
printf_(", ", {});
b.CreateBr(item);
b.SetInsertPoint(item);
emitPrint(b.CreateLoad(ty(et), b.CreateInBoundsGEP(ty(et), data, i)), et, true);
});
printf_("]", {});
} else if (t.kind == FType::Struct) {
printf_(t.info->name + "(", {});
bool first = true;
for (auto &fl : t.info->fields) {
printf_((first ? "" : ", ") + fl.name + ": ", {});
first = false;
emitPrint(b.CreateLoad(ty(fl.type), b.CreateStructGEP(t.info->llvm, a, fl.llvmIndex)), fl.type, true);
}
printf_(")", {});
}
b.CreateRetVoid();
return f;
}
// ---------- debug info ----------
DIType *Codegen::diType(const FType &t) {
std::string key = typeKey(t);
if (auto it = diTypes.find(key); it != diTypes.end()) return it->second;
DIType *d = nullptr;
auto basic = [&](const char *n, unsigned bits, unsigned enc) { return di->createBasicType(n, bits, enc); };
switch (t.kind) {
case FType::Void: return nullptr;
case FType::Bool: d = basic("bool", 8, dwarf::DW_ATE_boolean); break;
case FType::Char: d = basic("char", 8, dwarf::DW_ATE_signed_char); break;
case FType::I8: d = basic("i8", 8, dwarf::DW_ATE_signed); break;
case FType::I16: d = basic("i16", 16, dwarf::DW_ATE_signed); break;
case FType::I32: d = basic("i32", 32, dwarf::DW_ATE_signed); break;
case FType::I64: d = basic("int", 64, dwarf::DW_ATE_signed); break;
case FType::U8: d = basic("u8", 8, dwarf::DW_ATE_unsigned); break;
case FType::U16: d = basic("u16", 16, dwarf::DW_ATE_unsigned); break;
case FType::U32: d = basic("u32", 32, dwarf::DW_ATE_unsigned); break;
case FType::U64: d = basic("u64", 64, dwarf::DW_ATE_unsigned); break;
case FType::F32: d = basic("f32", 32, dwarf::DW_ATE_float); break;
case FType::F64: d = basic("float", 64, dwarf::DW_ATE_float); break;
case FType::Ptr:
case FType::Null: d = di->createPointerType(t.elem ? diType(*t.elem) : nullptr, 64, 0, std::nullopt, t.show()); break;
case FType::Fixed:
d = di->createArrayType(t.count * (uint64_t)dl->getTypeAllocSizeInBits(ty(*t.elem)), 0, diType(*t.elem),
di->getOrCreateArray({di->getOrCreateSubrange(0, t.count)}));
break;
case FType::Str:
case FType::Array: {
DIType *et = t.kind == FType::Str ? diType(FType::Char) : diType(*t.elem);
DIType *pt = di->createPointerType(et, 64);
DIType *i64 = diType(FType::I64);
DIFile *f = diFiles[0];
Metadata *els[] = {di->createMemberType(diUnit, "ptr", f, 0, 64, 64, 0, DINode::FlagZero, pt),
di->createMemberType(diUnit, "len", f, 0, 64, 64, 64, DINode::FlagZero, i64),
di->createMemberType(diUnit, "cap", f, 0, 64, 64, 128, DINode::FlagZero, i64)};
d = di->createStructType(diUnit, t.show(), f, 0, 192, 64, DINode::FlagZero, nullptr, di->getOrCreateArray(els));
break;
}
case FType::Struct: {
StructInfo *s = t.info;
DIFile *f = s->isC ? diFiles[0] : diFiles[s->pos.file];
auto *fwd = di->createReplaceableCompositeType(dwarf::DW_TAG_structure_type, s->name, diUnit, f, s->pos.line);
diTypes[key] = fwd;
const StructLayout *lay = dl->getStructLayout(s->llvm);
std::vector<Metadata *> els;
for (auto &fl : s->fields) {
uint64_t bits = dl->getTypeAllocSizeInBits(ty(fl.type));
els.push_back(di->createMemberType(diUnit, fl.name, f, s->pos.line, bits, 8, lay->getElementOffsetInBits(fl.llvmIndex),
DINode::FlagZero, diType(fl.type)));
}
DICompositeType *real = di->createStructType(diUnit, s->name, f, s->pos.line, lay->getSizeInBits(), 8,
DINode::FlagZero, nullptr, di->getOrCreateArray(els));
di->replaceTemporary(TempDIType(fwd), real);
d = real;
break;
}
case FType::Named: return nullptr;
}
diTypes[key] = d;
return d;
}
} // namespace finch

View file

@ -3,21 +3,28 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <string> #include <string>
#include <vector>
// Every source file the compiler has read; positions point into this list.
struct SourceFile {
std::string path;
std::string text;
};
inline std::vector<SourceFile> g_files;
inline int g_curFile = 0; // the file the lexer/parser is working on
// Finch stops at the first error and shows exactly where it is. // Finch stops at the first error and shows exactly where it is.
inline std::string g_file; [[noreturn]] inline void failAt(int file, int line, int col, const std::string &msg) {
inline std::string g_source; const SourceFile &f = g_files.at(file);
std::fprintf(stderr, "%s:%d:%d: error: %s\n", f.path.c_str(), line, col, msg.c_str());
[[noreturn]] inline void fail(int line, int col, const std::string &msg) {
std::fprintf(stderr, "%s:%d:%d: error: %s\n", g_file.c_str(), line, col, msg.c_str());
size_t start = 0; size_t start = 0;
for (int l = 1; l < line && start != std::string::npos; l++) { for (int l = 1; l < line && start != std::string::npos; l++) {
start = g_source.find('\n', start); start = f.text.find('\n', start);
if (start != std::string::npos) start++; if (start != std::string::npos) start++;
} }
if (start != std::string::npos && start < g_source.size()) { if (line > 0 && start != std::string::npos && start < f.text.size()) {
std::string text = g_source.substr(start, g_source.find('\n', start) - start); std::string text = f.text.substr(start, f.text.find('\n', start) - start);
std::string pad; std::string pad;
int c = 1; // columns count characters; skip UTF-8 continuation bytes int c = 1; // columns count characters; skip UTF-8 continuation bytes
for (size_t k = 0; k < text.size() && c < col; k++) { for (size_t k = 0; k < text.size() && c < col; k++) {
@ -29,3 +36,5 @@ inline std::string g_source;
} }
std::exit(1); std::exit(1);
} }
[[noreturn]] inline void fail(int line, int col, const std::string &msg) { failAt(g_curFile, line, col, msg); }

View file

@ -9,12 +9,13 @@ static const std::unordered_map<std::string, Tok> keywords = {
{"else", Tok::Else}, {"while", Tok::While}, {"for", Tok::For}, {"else", Tok::Else}, {"while", Tok::While}, {"for", Tok::For},
{"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue}, {"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue},
{"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null}, {"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null},
{"import", Tok::Import}, {"link", Tok::Link}, {"import", Tok::Import}, {"link", Tok::Link}, {"struct", Tok::Struct},
{"defer", Tok::Defer},
}; };
std::vector<Token> lex(const std::string &src, const std::string &file) { std::vector<Token> lex(int file) {
g_file = file; g_curFile = file;
g_source = src; const std::string &src = g_files[file].text;
std::vector<Token> out; std::vector<Token> out;
size_t i = 0; size_t i = 0;
int line = 1, col = 1; int line = 1, col = 1;
@ -126,10 +127,7 @@ std::vector<Token> lex(const std::string &src, const std::string &file) {
case '!': n == '=' ? two(Tok::NotEq) : one(Tok::Not); break; case '!': n == '=' ? two(Tok::NotEq) : one(Tok::Not); break;
case '<': n == '=' ? two(Tok::LessEq) : n == '<' ? two(Tok::Shl) : one(Tok::Less); break; case '<': n == '=' ? two(Tok::LessEq) : n == '<' ? two(Tok::Shl) : one(Tok::Less); break;
case '>': n == '=' ? two(Tok::GreaterEq) : n == '>' ? two(Tok::Shr) : one(Tok::Greater); break; case '>': n == '=' ? two(Tok::GreaterEq) : n == '>' ? two(Tok::Shr) : one(Tok::Greater); break;
case ':': case ':': n == '=' ? two(Tok::Declare) : one(Tok::Colon); break;
if (n != '=') fail(line, col, "unexpected ':' (did you mean ':='?)");
two(Tok::Declare);
break;
case '&': n == '&' ? two(Tok::And) : one(Tok::Amp); break; case '&': n == '&' ? two(Tok::And) : one(Tok::Amp); break;
case '|': n == '|' ? two(Tok::Or) : one(Tok::Pipe); break; case '|': n == '|' ? two(Tok::Or) : one(Tok::Pipe); break;
default: { default: {
@ -169,6 +167,9 @@ const char *tokName(Tok t) {
case Tok::Null: return "'null'"; case Tok::Null: return "'null'";
case Tok::Import: return "'import'"; case Tok::Import: return "'import'";
case Tok::Link: return "'link'"; case Tok::Link: return "'link'";
case Tok::Struct: return "'struct'";
case Tok::Defer: return "'defer'";
case Tok::Colon: return "':'";
case Tok::LParen: return "'('"; case Tok::LParen: return "'('";
case Tok::RParen: return "')'"; case Tok::RParen: return "')'";
case Tok::LBrace: return "'{'"; case Tok::LBrace: return "'{'";

View file

@ -8,10 +8,10 @@ enum class Tok {
Ident, Int, Float, String, Char, Ident, Int, Float, String, Char,
// keywords // keywords
Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer,
// punctuation // punctuation
LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Dot, DotDot, Arrow, LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot, DotDot, Arrow,
// operators // operators
Plus, Minus, Star, Slash, Percent, Plus, Minus, Star, Slash, Percent,
@ -30,5 +30,6 @@ struct Token {
bool newlineBefore; // token starts a new line: ends the previous statement bool newlineBefore; // token starts a new line: ends the previous statement
}; };
std::vector<Token> lex(const std::string &src, const std::string &file); // Lexes g_files[file].text.
std::vector<Token> lex(int file);
const char *tokName(Tok t); const char *tokName(Tok t);

View file

@ -1,14 +1,16 @@
#include "cimport.h" #include "cimport.h"
#include "codegen.h" #include "codegen.h"
#include "error.h"
#include "lexer.h" #include "lexer.h"
#include "parser.h" #include "parser.h"
#include <llvm/Config/llvm-config.h>
#include <llvm/IR/LegacyPassManager.h> #include <llvm/IR/LegacyPassManager.h>
#include <llvm/MC/TargetRegistry.h> #include <llvm/MC/TargetRegistry.h>
#include <llvm/Passes/PassBuilder.h> #include <llvm/Passes/PassBuilder.h>
#include <llvm/Support/FileSystem.h> #include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <llvm/Support/TargetSelect.h> #include <llvm/Support/TargetSelect.h>
#include <llvm/Config/llvm-config.h>
#include <llvm/Target/TargetMachine.h> #include <llvm/Target/TargetMachine.h>
#include <llvm/TargetParser/Host.h> #include <llvm/TargetParser/Host.h>
@ -18,38 +20,90 @@
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
#include <map>
#include <sstream> #include <sstream>
#include <sys/wait.h>
#include <string> #include <string>
#include <sys/wait.h>
#include <vector> #include <vector>
#include "rt_source.inc" // kRuntimeSource: runtime/finch_rt.c, embedded at build time
using namespace llvm; using namespace llvm;
static void usage() { static void usage() {
std::fprintf(stderr, std::fprintf(stderr,
"Finch " FINCH_VERSION " - small, quick, sharp.\n\n" "Finch " FINCH_VERSION " - small, quick, sharp.\n\n"
"usage:\n" "usage:\n"
" finch run <file.fn> compile and run\n" " finch run <file.fn> [args...] compile and run\n"
" finch build <file.fn> [-o name] compile to a program\n" " finch build <file.fn> [-o name] compile to a program\n"
" finch ir <file.fn> show the generated LLVM IR\n"
" finch version show the version\n"
"\n" "\n"
" -l <lib> link a C library, same as link \"lib\" in the file\n" " -l <lib> link a C library, same as link \"lib\" in the file\n"
" -O0 skip optimizations (to read the raw IR)\n" " -g add debug info (for gdb / lldb)\n"
" finch ir <file.fn> show the generated LLVM IR\n" " -O0 skip optimizations (to read the raw IR)\n");
" finch version show the version\n");
std::exit(1); std::exit(1);
} }
static std::string readFile(const std::string &path) { static bool readFile(const std::string &path, std::string &out) {
std::ifstream in(path); std::ifstream in(path);
if (!in) { if (!in) return false;
std::fprintf(stderr, "error: can't open '%s'\n", path.c_str());
std::exit(1);
}
std::stringstream ss; std::stringstream ss;
ss << in.rdbuf(); ss << in.rdbuf();
return ss.str(); out = ss.str();
return true;
} }
static std::string dirOf(const std::string &path) {
size_t slash = path.find_last_of('/');
return slash == std::string::npos ? "." : path.substr(0, slash);
}
// ---------- loading the program and its modules ----------
struct Loader {
std::vector<Program> progs;
std::map<std::string, bool> loaded; // module name -> seen
void load(const std::string &path, const std::string &module, Pos from) {
std::string text;
if (!readFile(path, text)) {
if (from.line) failAt(from.file, from.line, from.col, "can't find the module '" + module + "' (looked for " + path + ")");
std::fprintf(stderr, "error: can't open '%s'\n", path.c_str());
std::exit(1);
}
int idx = (int)g_files.size();
g_files.push_back({path, text});
Program p = parse(lex(idx), idx);
p.module = module;
p.path = path;
std::vector<Import> mods;
for (const Import &im : p.imports)
if (!im.isC) mods.push_back(im);
progs.push_back(std::move(p));
for (const Import &im : mods) {
if (im.path == "main") failAt(im.pos.file, im.pos.line, im.pos.col, "'main' can't be imported");
if (loaded[im.path]) continue;
loaded[im.path] = true;
load(findModule(dirOf(path), im.path), im.path, im.pos);
}
}
// next to the importing file, then in $FINCH_PATH folders
static std::string findModule(const std::string &dir, const std::string &name) {
std::string here = dir + "/" + name + ".fn";
if (sys::fs::exists(here)) return here;
if (const char *fp = std::getenv("FINCH_PATH")) {
std::stringstream ss(fp);
for (std::string d; std::getline(ss, d, ':');)
if (!d.empty() && sys::fs::exists(d + "/" + name + ".fn")) return d + "/" + name + ".fn";
}
return here;
}
};
// ---------- LLVM ----------
static void optimize(Module &mod, TargetMachine *tm) { static void optimize(Module &mod, TargetMachine *tm) {
LoopAnalysisManager lam; LoopAnalysisManager lam;
FunctionAnalysisManager fam; FunctionAnalysisManager fam;
@ -93,6 +147,8 @@ static void emitObject(Module &mod, TargetMachine *tm, const std::string &path)
pm.run(mod); pm.run(mod);
} }
// ---------- linking ----------
// Run a shell command; returns its exit status, everything it printed goes to `output`. // Run a shell command; returns its exit status, everything it printed goes to `output`.
static int capture(const std::string &cmd, std::string &output) { static int capture(const std::string &cmd, std::string &output) {
FILE *p = popen((cmd + " 2>&1").c_str(), "r"); FILE *p = popen((cmd + " 2>&1").c_str(), "r");
@ -104,6 +160,38 @@ static int capture(const std::string &cmd, std::string &output) {
return WIFEXITED(status) ? WEXITSTATUS(status) : -1; return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
} }
static std::string ccName() {
const char *cc = std::getenv("CC");
return cc ? cc : "cc";
}
// The runtime is compiled once per version and kept in ~/.cache/finch.
static std::string runtimeObject() {
std::string dir;
if (const char *x = std::getenv("XDG_CACHE_HOME")) dir = std::string(x) + "/finch";
else if (const char *h = std::getenv("HOME")) dir = std::string(h) + "/.cache/finch";
else dir = "/tmp/finch-cache";
size_t hash = std::hash<std::string>()(std::string(kRuntimeSource) + FINCH_VERSION + ccName());
std::string obj = dir + "/rt-" + std::to_string(hash) + ".o";
if (sys::fs::exists(obj)) return obj;
sys::fs::create_directories(dir);
std::string src = obj + ".c", part = obj + ".part";
{
std::ofstream out(src);
out << kRuntimeSource;
}
std::string output;
int status = capture(ccName() + " -O2 -fPIC -c '" + src + "' -o '" + part + "'", output);
std::remove(src.c_str());
if (status != 0) {
std::fprintf(stderr, "%serror: couldn't build the Finch runtime (is a C compiler installed?)\n", output.c_str());
std::exit(1);
}
sys::fs::rename(part, obj);
return obj;
}
// pkg-config knows the right paths for a library if it has one; otherwise plain -l<name>. // pkg-config knows the right paths for a library if it has one; otherwise plain -l<name>.
static std::string libFlags(const std::string &lib) { static std::string libFlags(const std::string &lib) {
std::string out; std::string out;
@ -151,7 +239,6 @@ static std::string guessLib(const std::string &header) {
for (const std::string &lib : missingLibs) for (const std::string &lib : missingLibs)
std::fprintf(stderr, "error: the library '%s' wasn't found. Is it installed? (on Nix: add it to shell.nix)\n", lib.c_str()); std::fprintf(stderr, "error: the library '%s' wasn't found. Is it installed? (on Nix: add it to shell.nix)\n", lib.c_str());
// group the missing functions by the header they came from
std::vector<std::pair<std::string, std::vector<std::string>>> byHeader; std::vector<std::pair<std::string, std::vector<std::string>>> byHeader;
for (const std::string &name : missing) { for (const std::string &name : missing) {
auto f = c.fns.find(name); auto f = c.fns.find(name);
@ -178,14 +265,34 @@ static std::string guessLib(const std::string &header) {
std::exit(1); std::exit(1);
} }
static bool endsWith(const std::string &s, const char *x) {
size_t n = std::strlen(x);
return s.size() > n && s.compare(s.size() - n, n, x) == 0;
}
static void link(const std::string &obj, const std::string &exe, const std::vector<std::string> &libs, static void link(const std::string &obj, const std::string &exe, const std::vector<std::string> &libs,
const CImports &c) { const CImports &c) {
const char *cc = std::getenv("CC"); std::string cmd = ccName() + " '" + obj + "' '" + runtimeObject() + "' -o '" + exe + "' -lm";
std::string cmd = std::string(cc ? cc : "cc") + " '" + obj + "' -o '" + exe + "' -lm"; std::vector<std::string> temps;
for (const std::string &l : libs) cmd += libFlags(l); for (const std::string &l : libs) {
if (endsWith(l, ".c")) { // compile the user's C file
std::string o = obj + "." + std::to_string(temps.size()) + ".o", output;
if (capture(ccName() + " -O2 -fPIC -c '" + l + "' -o '" + o + "'", output) != 0) {
std::fprintf(stderr, "%serror: couldn't compile %s\n", output.c_str(), l.c_str());
std::exit(1);
}
temps.push_back(o);
cmd += " '" + o + "'";
} else if (endsWith(l, ".o") || endsWith(l, ".a")) {
cmd += " '" + l + "'";
} else {
cmd += libFlags(l);
}
}
std::string output; std::string output;
int status = capture(cmd, output); int status = capture(cmd, output);
std::remove(obj.c_str()); std::remove(obj.c_str());
for (const std::string &t : temps) std::remove(t.c_str());
if (status == 127) { if (status == 127) {
std::fprintf(stderr, "error: no C compiler found to link with (install gcc or clang, or set CC)\n"); std::fprintf(stderr, "error: no C compiler found to link with (install gcc or clang, or set CC)\n");
std::exit(1); std::exit(1);
@ -201,6 +308,12 @@ static std::string stem(const std::string &path) {
return dot == std::string::npos ? name : name.substr(0, dot); return dot == std::string::npos ? name : name.substr(0, dot);
} }
static std::string shellQuote(const std::string &s) {
std::string out = "'";
for (char ch : s) out += ch == '\'' ? std::string("'\\''") : std::string(1, ch);
return out + "'";
}
int main(int argc, char **argv) { int main(int argc, char **argv) {
if (argc == 2 && (std::string(argv[1]) == "version" || std::string(argv[1]) == "--version")) { if (argc == 2 && (std::string(argv[1]) == "version" || std::string(argv[1]) == "--version")) {
std::printf("finch %s (LLVM %s)\n", FINCH_VERSION, LLVM_VERSION_STRING); std::printf("finch %s (LLVM %s)\n", FINCH_VERSION, LLVM_VERSION_STRING);
@ -208,26 +321,40 @@ int main(int argc, char **argv) {
} }
if (argc < 3) usage(); if (argc < 3) usage();
std::string cmd = argv[1], file = argv[2]; std::string cmd = argv[1], file = argv[2];
if (cmd != "run" && cmd != "build" && cmd != "ir") usage();
std::string out = stem(file); std::string out = stem(file);
std::vector<std::string> libs; std::vector<std::string> libs, programArgs;
bool optimizeCode = true; bool optimizeCode = true, debug = false;
for (int i = 3; i < argc; i++) { for (int i = 3; i < argc; i++) {
std::string a = argv[i]; std::string a = argv[i];
if (a == "-o" && i + 1 < argc) out = argv[++i]; if (cmd == "run" && !programArgs.empty()) programArgs.push_back(a);
else if (a == "-o" && i + 1 < argc && cmd == "build") out = argv[++i];
else if (a == "-O0") optimizeCode = false; else if (a == "-O0") optimizeCode = false;
else if (a == "-g") debug = true;
else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]); else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]);
else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2)); else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2));
else if (cmd == "run") programArgs.push_back(a);
else usage(); else usage();
} }
if (cmd != "run" && cmd != "build" && cmd != "ir") usage();
Program prog = parse(lex(readFile(file), file)); Loader loader;
for (const Link &l : prog.links) libs.push_back(l.lib); loader.load(file, "", Pos{});
size_t slash = file.find_last_of('/'); std::vector<Import> allImports;
CImports c = importHeaders(prog.imports, slash == std::string::npos ? "." : file.substr(0, slash)); std::vector<std::string> dirs;
for (const Program &p : loader.progs) {
allImports.insert(allImports.end(), p.imports.begin(), p.imports.end());
for (const Link &l : p.links) {
bool file = endsWith(l.lib, ".c") || endsWith(l.lib, ".o") || endsWith(l.lib, ".a");
std::string path = file && l.lib[0] != '/' ? dirOf(p.path) + "/" + l.lib : l.lib;
if (file && !sys::fs::exists(path)) failAt(l.pos.file, l.pos.line, l.pos.col, "can't find the file '" + path + "'");
libs.push_back(path);
}
if (std::find(dirs.begin(), dirs.end(), dirOf(p.path)) == dirs.end()) dirs.push_back(dirOf(p.path));
}
CImports c = importHeaders(allImports, dirs);
LLVMContext ctx; LLVMContext ctx;
TargetMachine *tm = hostMachine(); TargetMachine *tm = hostMachine();
std::unique_ptr<Module> mod = generate(prog, c, ctx, *tm); std::unique_ptr<Module> mod = generate(loader.progs, c, ctx, *tm, debug);
if (optimizeCode) optimize(*mod, tm); if (optimizeCode) optimize(*mod, tm);
if (cmd == "ir") { if (cmd == "ir") {
@ -246,7 +373,9 @@ int main(int argc, char **argv) {
link(obj, out, libs, c); link(obj, out, libs, c);
if (cmd == "run") { if (cmd == "run") {
int status = std::system(("'" + out + "'").c_str()); std::string line = shellQuote(out);
for (const std::string &a : programArgs) line += " " + shellQuote(a);
int status = std::system(line.c_str());
std::remove(out.c_str()); std::remove(out.c_str());
if (WIFSIGNALED(status)) { if (WIFSIGNALED(status)) {
std::fprintf(stderr, "\nthe program crashed: %s\n", strsignal(WTERMSIG(status))); std::fprintf(stderr, "\nthe program crashed: %s\n", strsignal(WTERMSIG(status)));

View file

@ -5,6 +5,7 @@
#include <cerrno> #include <cerrno>
#include <climits> #include <climits>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <unordered_map> #include <unordered_map>
bool typeFromName(const std::string &name, Type &out) { bool typeFromName(const std::string &name, Type &out) {
@ -29,28 +30,32 @@ bool isTypeName(const std::string &name) {
class Parser { class Parser {
public: public:
explicit Parser(const std::vector<Token> &t) : toks(t) {} Parser(const std::vector<Token> &t, int f) : toks(t), file(f) {}
Program program() { Program program() {
Program p; Program p;
while (!at(Tok::End)) { while (!at(Tok::End)) {
if (at(Tok::Import)) p.imports.push_back(import()); switch (cur().kind) {
else if (at(Tok::Link)) p.links.push_back(link()); case Tok::Import: p.imports.push_back(import()); break;
else p.fns.push_back(function()); case Tok::Link: p.links.push_back(link()); break;
case Tok::Struct: p.structs.push_back(structDecl()); break;
default: p.fns.push_back(function()); break;
}
} }
return p; return p;
} }
private: private:
const std::vector<Token> &toks; const std::vector<Token> &toks;
int file;
size_t i = 0; size_t i = 0;
int parenDepth = 0; // inside ( ) newlines don't end anything int parenDepth = 0; // inside ( ) and [ ] newlines don't end anything
const Token &cur() const { return toks[i]; } const Token &cur() const { return toks[i]; }
const Token &peekTok(size_t off = 1) const { return toks[std::min(i + off, toks.size() - 1)]; } const Token &peekTok(size_t off = 1) const { return toks[std::min(i + off, toks.size() - 1)]; }
const Token &next() { return toks[i++]; } const Token &next() { return toks[i++]; }
bool at(Tok k) const { return cur().kind == k; } bool at(Tok k) const { return cur().kind == k; }
Pos pos() const { return {cur().line, cur().col}; } Pos pos() const { return {cur().line, cur().col, file}; }
std::string describe(const Token &t) const { std::string describe(const Token &t) const {
return t.kind == Tok::Ident ? "'" + t.text + "'" : tokName(t.kind); return t.kind == Tok::Ident ? "'" + t.text + "'" : tokName(t.kind);
@ -71,27 +76,44 @@ private:
fail(cur().line, cur().col, "unexpected " + describe(cur()) + " " + ctx); fail(cur().line, cur().col, "unexpected " + describe(cur()) + " " + ctx);
} }
// A token can continue an expression only if it is on the same line (or we're inside parens). // A token can continue an expression only if it is on the same line (or we're inside brackets).
bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; } bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; }
// int, u8, ptr, ptr[int], ptr[ptr[u8]] ... // int, u8, ptr, ptr[int], []str, Point, math.Vec ...
Type type() { Type type() {
if (!at(Tok::Ident) || !isTypeName(cur().text)) if (accept(Tok::LBracket)) {
unexpected("(expected a type like int, float, u8, str or ptr[int])"); expect(Tok::RBracket, "']' (an array type looks like []int)");
return Type::arrayOf(type());
}
if (!at(Tok::Ident)) unexpected("(expected a type like int, str, []int, ptr[int] or a struct name)");
std::string name = next().text;
Type t; Type t;
typeFromName(next().text, t); if (typeFromName(name, t)) {
if (t.kind == Type::Ptr && accept(Tok::LBracket)) { if (t.kind == Type::Ptr && accept(Tok::LBracket)) {
t = Type::ptrTo(type()); t = Type::ptrTo(type());
expect(Tok::RBracket); expect(Tok::RBracket);
}
return t;
}
t = Type(Type::Named);
t.name = name;
if (at(Tok::Dot) && peekTok().kind == Tok::Ident) { // module.Struct
next();
t.module = name;
t.name = next().text;
} }
return t; return t;
} }
// Does a declaration like `int x` or `ptr[int] p` start here? // Does a declaration like `int x`, `ptr[int] p`, `[]int a`, `Point p` or `math.Vec v` start here?
bool atDeclaration() const { bool atDeclaration() const {
if (!at(Tok::Ident) || !isTypeName(cur().text)) return false; if (at(Tok::LBracket)) return peekTok().kind == Tok::RBracket;
if (!at(Tok::Ident)) return false;
Tok n = peekTok().kind; Tok n = peekTok().kind;
return n == Tok::Ident || (cur().text == "ptr" && n == Tok::LBracket); if (n == Tok::Ident) return !peekTok().newlineBefore;
if (cur().text == "ptr" && n == Tok::LBracket) return true;
return n == Tok::Dot && peekTok(2).kind == Tok::Ident && peekTok(3).kind == Tok::Ident &&
!peekTok(3).newlineBefore;
} }
// ---------- top level ---------- // ---------- top level ----------
@ -100,9 +122,12 @@ private:
Import im; Import im;
im.pos = pos(); im.pos = pos();
expect(Tok::Import); expect(Tok::Import);
if (at(Tok::Ident)) if (at(Tok::Ident)) {
fail(im.pos.line, im.pos.col, "Finch modules are not ready yet; for C headers write import \"stdio.h\""); im.isC = false;
im.path = expect(Tok::String, "a header name in quotes, like \"stdio.h\"").text; im.path = next().text;
} else {
im.path = expect(Tok::String, "a module name (import math) or a C header in quotes (import \"stdio.h\")").text;
}
return im; return im;
} }
@ -111,17 +136,44 @@ private:
l.pos = pos(); l.pos = pos();
expect(Tok::Link); expect(Tok::Link);
l.lib = expect(Tok::String, "a library name in quotes, like link \"glfw\"").text; l.lib = expect(Tok::String, "a library name in quotes, like link \"glfw\"").text;
auto ends = [&](const char *x) { size_t n = std::strlen(x); return l.lib.size() > n && l.lib.compare(l.lib.size() - n, n, x) == 0; };
if (ends(".c") || ends(".o") || ends(".a")) return l; // your own C code, next to the .fn file
if (l.lib.rfind("lib", 0) == 0 || l.lib.find(".so") != std::string::npos || l.lib.find('/') != std::string::npos) if (l.lib.rfind("lib", 0) == 0 || l.lib.find(".so") != std::string::npos || l.lib.find('/') != std::string::npos)
fail(l.pos.line, l.pos.col, "write just the library's name: for libglfw.so that's link \"glfw\""); fail(l.pos.line, l.pos.col, "write just the library's name: for libglfw.so that's link \"glfw\"");
return l; return l;
} }
StructDecl structDecl() {
StructDecl s;
s.pos = pos();
expect(Tok::Struct);
s.name = expect(Tok::Ident, "a struct name").text;
if (isTypeName(s.name)) fail(s.pos.line, s.pos.col, "'" + s.name + "' is a built-in type name, pick another");
Pos open = pos();
expect(Tok::LBrace);
while (!at(Tok::RBrace)) {
if (at(Tok::End)) fail(open.line, open.col, "this '{' is never closed (missing '}')");
Field f;
f.pos = pos();
if (!atDeclaration()) unexpected("(a field looks like: int x or str name = \"default\")");
f.type = type();
f.name = expect(Tok::Ident, "a field name").text;
for (const Field &other : s.fields)
if (other.name == f.name) fail(f.pos.line, f.pos.col, "the field '" + f.name + "' is listed twice");
if (accept(Tok::Assign)) f.init = expr();
s.fields.push_back(std::move(f));
endOfStatement();
}
next();
return s;
}
FnDecl function() { FnDecl function() {
FnDecl fn; FnDecl fn;
fn.pos = pos(); fn.pos = pos();
if (at(Tok::Ident) && isTypeName(cur().text)) if (at(Tok::Ident) && isTypeName(cur().text))
fail(fn.pos.line, fn.pos.col, "functions start with 'fn', like: fn add(int a, int b) -> int { ... }"); fail(fn.pos.line, fn.pos.col, "functions start with 'fn', like: fn add(int a, int b) -> int { ... }");
expect(Tok::Fn, "'fn' (every function starts with fn)"); expect(Tok::Fn, "'fn', 'struct', 'import' or 'link' (every function starts with fn)");
fn.name = expect(Tok::Ident, "a function name").text; fn.name = expect(Tok::Ident, "a function name").text;
expect(Tok::LParen); expect(Tok::LParen);
@ -177,12 +229,20 @@ private:
} }
case Tok::For: { case Tok::For: {
next(); next();
auto s = std::make_unique<ForStmt>(p); std::string var = expect(Tok::Ident, "a loop variable name").text;
s->var = expect(Tok::Ident, "a loop variable name").text;
expect(Tok::In); expect(Tok::In);
s->from = expr(); ExprPtr first = expr();
expect(Tok::DotDot, "'..' (like 'for i in 0..10')"); if (accept(Tok::DotDot)) { // for i in 0..10
s->to = expr(); auto s = std::make_unique<ForStmt>(p);
s->var = var;
s->from = std::move(first);
s->to = expr();
s->body = block();
return s;
}
auto s = std::make_unique<ForEachStmt>(p); // for x in list
s->var = var;
s->list = std::move(first);
s->body = block(); s->body = block();
return s; return s;
} }
@ -194,19 +254,25 @@ private:
} }
case Tok::Break: next(); return std::make_unique<BreakStmt>(p); case Tok::Break: next(); return std::make_unique<BreakStmt>(p);
case Tok::Continue: next(); return std::make_unique<ContinueStmt>(p); case Tok::Continue: next(); return std::make_unique<ContinueStmt>(p);
case Tok::Defer: {
next();
if (at(Tok::Defer)) fail(p.line, p.col, "defer inside defer is not allowed");
return std::make_unique<DeferStmt>(p, statement());
}
case Tok::LBrace: return block(); case Tok::LBrace: return block();
case Tok::Fn: fail(p.line, p.col, "functions can't be inside other functions (move it out)"); case Tok::Fn: fail(p.line, p.col, "functions can't be inside other functions (move it out)");
case Tok::Struct: fail(p.line, p.col, "structs go outside functions, at the top level of the file");
case Tok::Import: case Tok::Import:
case Tok::Link: fail(p.line, p.col, "import and link go at the top of the file, outside functions"); case Tok::Link: fail(p.line, p.col, "import and link go at the top of the file, outside functions");
default: break; default: break;
} }
// int x = 5 // int x = 5, Point p, []int a
if (atDeclaration()) { if (atDeclaration()) {
auto s = std::make_unique<VarDeclStmt>(p); auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = true; s->hasType = true;
s->type = type(); s->type = type();
s->name = next().text; s->name = expect(Tok::Ident, "a variable name").text;
if (accept(Tok::Assign)) s->init = expr(); if (accept(Tok::Assign)) s->init = expr();
return s; return s;
} }
@ -223,7 +289,7 @@ private:
ExprPtr e = expr(); ExprPtr e = expr();
// x = 5, p.value += 1, ... // x = 5, p.value += 1, a[i] = 2, ...
char op = 0; char op = 0;
switch (cur().kind) { switch (cur().kind) {
case Tok::Assign: op = '='; break; case Tok::Assign: op = '='; break;
@ -237,8 +303,8 @@ private:
default: break; default: break;
} }
if (op && sameLine()) { if (op && sameLine()) {
if (e->kind != ExprKind::Var && e->kind != ExprKind::Member) if (e->kind != ExprKind::Var && e->kind != ExprKind::Member && e->kind != ExprKind::Index)
fail(p.line, p.col, "only a variable or p.value can be assigned to"); fail(p.line, p.col, "only a variable, a field, an element or p.value can be assigned to");
auto s = std::make_unique<AssignStmt>(p); auto s = std::make_unique<AssignStmt>(p);
s->target = std::move(e); s->target = std::move(e);
s->op = op; s->op = op;
@ -247,7 +313,8 @@ private:
return s; return s;
} }
if (e->kind != ExprKind::Call) fail(p.line, p.col, "this value is computed but never used"); if (e->kind != ExprKind::Call && e->kind != ExprKind::Method)
fail(p.line, p.col, "this value is computed but never used");
return std::make_unique<ExprStmt>(p, std::move(e)); return std::make_unique<ExprStmt>(p, std::move(e));
} }
@ -355,29 +422,51 @@ private:
return postfix(); return postfix();
} }
// p.value // p.value, a.len, a[i], a.push(x), math.sqrt(2)
ExprPtr postfix() { ExprPtr postfix() {
ExprPtr e = primary(); ExprPtr e = primary();
while (sameLine() && at(Tok::Dot)) { while (sameLine()) {
Pos p = pos(); Pos p = pos();
next(); if (accept(Tok::Dot)) {
e = std::make_unique<MemberExpr>(p, std::move(e), expect(Tok::Ident, "a name after '.'").text); std::string name = expect(Tok::Ident, "a name after '.'").text;
if (at(Tok::LParen) && !cur().newlineBefore) {
auto m = std::make_unique<MethodExpr>(p, std::move(e), name);
args(m->args, m->argNames);
e = std::move(m);
} else {
e = std::make_unique<MemberExpr>(p, std::move(e), name);
}
} else if (at(Tok::LBracket)) {
next();
parenDepth++;
ExprPtr index = expr();
parenDepth--;
expect(Tok::RBracket);
e = std::make_unique<IndexExpr>(p, std::move(e), std::move(index));
} else {
return e;
}
} }
return e; return e;
} }
// name(arg, arg, ...) -- we are at the '(' // ( arg, name: arg, ... ) -- we are at the '('
ExprPtr callArgs(Pos p, const std::string &name) { void args(std::vector<ExprPtr> &out, std::vector<std::string> &names) {
auto call = std::make_unique<CallExpr>(p, name);
expect(Tok::LParen); expect(Tok::LParen);
parenDepth++; parenDepth++;
if (!at(Tok::RParen)) { if (!at(Tok::RParen)) {
do call->args.push_back(expr()); do {
while (accept(Tok::Comma)); std::string name;
if (at(Tok::Ident) && peekTok().kind == Tok::Colon) {
name = next().text;
next();
}
names.push_back(name);
out.push_back(expr());
} while (accept(Tok::Comma));
} }
parenDepth--; parenDepth--;
expect(Tok::RParen); expect(Tok::RParen);
return call;
} }
ExprPtr primary() { ExprPtr primary() {
@ -406,9 +495,25 @@ private:
expect(Tok::RParen); expect(Tok::RParen);
return e; return e;
} }
case Tok::LBracket: { // [1, 2, 3]
next();
auto a = std::make_unique<ArrayLitExpr>(p);
parenDepth++;
while (!at(Tok::RBracket)) {
a->elems.push_back(expr());
if (!accept(Tok::Comma)) break; // a trailing comma is fine
}
parenDepth--;
expect(Tok::RBracket);
return a;
}
case Tok::Ident: { case Tok::Ident: {
next(); next();
if (at(Tok::LParen) && !cur().newlineBefore) return callArgs(p, t.text); if (at(Tok::LParen) && !cur().newlineBefore) {
auto call = std::make_unique<CallExpr>(p, t.text);
args(call->args, call->argNames);
return call;
}
if (isTypeName(t.text)) if (isTypeName(t.text))
fail(p.line, p.col, "a type name alone is not a value (did you mean " + t.text + "(...)?)"); fail(p.line, p.col, "a type name alone is not a value (did you mean " + t.text + "(...)?)");
return std::make_unique<VarExpr>(p, t.text); return std::make_unique<VarExpr>(p, t.text);
@ -420,6 +525,6 @@ private:
} // namespace } // namespace
Program parse(const std::vector<Token> &tokens) { Program parse(const std::vector<Token> &tokens, int file) {
return Parser(tokens).program(); return Parser(tokens, file).program();
} }

View file

@ -3,4 +3,5 @@
#include "ast.h" #include "ast.h"
#include "lexer.h" #include "lexer.h"
Program parse(const std::vector<Token> &tokens); // Parses one file's tokens; `file` is its index in g_files.
Program parse(const std::vector<Token> &tokens, int file);

View file

@ -0,0 +1,4 @@
// expect: compare the parts one by one
fn main() {
print([1] == [1])
}

4
tests/fail/bad_int.fn Normal file
View file

@ -0,0 +1,4 @@
// expect: can't turn "12x" into int
fn main() {
print(int("12x"))
}

View file

@ -0,0 +1,4 @@
// expect: return can't be used inside defer
fn main() {
defer return
}

View file

@ -0,0 +1,6 @@
// expect: the loop variable 'x' can't be changed
fn main() {
for x in [1, 2] {
x = 3
}
}

View file

@ -0,0 +1,4 @@
// expect: an empty [] needs a type
fn main() {
a := []
}

5
tests/fail/free_value.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: arrays, str and structs are freed by Finch
fn main() {
x := 5
free(x)
}

View file

@ -0,0 +1,5 @@
// expect: runtime error: index 5 is out of range (the length is 3)
fn main() {
a := [1, 2, 3]
print(a[5])
}

View file

@ -0,0 +1,3 @@
// expect: can't find the module 'nothere'
import nothere
fn main() {}

View file

@ -0,0 +1,4 @@
// expect: to use a module, add import geometry
fn main() {
v := geometry.Vec(1, 2)
}

5
tests/fail/pop_empty.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: runtime error: pop() on an empty array
fn main() {
[]int a
a.pop()
}

5
tests/fail/push_type.fn Normal file
View file

@ -0,0 +1,5 @@
// expect: the element must be int, but this is str
fn main() {
a := [1, 2]
a.push("x")
}

6
tests/fail/str_null.fn Normal file
View file

@ -0,0 +1,6 @@
// expect: a str is never null
fn main() {
s := "a"
if s == null {
}
}

View file

@ -0,0 +1,4 @@
// expect: turn the other side into text first: str(...)
fn main() {
s := "a" + 5
}

View file

@ -0,0 +1,8 @@
// expect: P has 2 fields, but got 1 values
struct P {
int x
int y
}
fn main() {
p := P(1)
}

View file

@ -0,0 +1,8 @@
// expect: the struct P has no field 'y' (it has: x)
struct P {
int x
}
fn main() {
p := P(1)
print(p.y)
}

View file

@ -0,0 +1,5 @@
// expect: contains itself; use ptr[A] or []A
struct A {
A inner
}
fn main() {}

View file

@ -1,27 +1,45 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Finch test suite. # Finch test suite.
# tests/run/*.fn must compile and print exactly what is in the matching .out file # tests/run/*.fn must compile and print exactly what is in the matching .out file
# (files without `fn main` are modules or helpers and are skipped;
# a matching .in file is fed to the program as its input)
# tests/fail/*.fn must fail; the first line is "// expect: <text that must appear in the error>" # tests/fail/*.fn must fail; the first line is "// expect: <text that must appear in the error>"
# MEMCHECK=1 tests/run.sh also runs every program under valgrind: no leaks, no bad memory access
set -u set -u
cd "$(dirname "$0")" cd "$(dirname "$0")"
FINCH=${FINCH:-../build/finch} FINCH=${FINCH:-../build/finch}
pass=0; failed=0 pass=0; failed=0
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
for f in run/*.fn; do for f in run/*.fn; do
grep -q "^fn main" "$f" || continue
want="${f%.fn}.out" want="${f%.fn}.out"
got=$("$FINCH" run "$f" 2>&1) input="${f%.fn}.in"
if [ "$got" == "$(cat "$want")" ]; then [ -f "$input" ] || input=/dev/null
pass=$((pass + 1)) got=$("$FINCH" run "$f" < "$input" 2>&1)
else if [ "$got" != "$(cat "$want")" ]; then
failed=$((failed + 1)) failed=$((failed + 1))
echo "FAIL $f" echo "FAIL $f"
diff <(echo "$got") "$want" | head -20 diff <(echo "$got") "$want" | head -20
continue
fi fi
if [ "${MEMCHECK:-0}" = 1 ]; then
exe="$tmp/$(basename "${f%.fn}")"
"$FINCH" build "$f" -o "$exe" >/dev/null 2>&1
if ! valgrind -q --leak-check=full --errors-for-leak-kinds=all --error-exitcode=99 "$exe" < "$input" >/dev/null 2>"$tmp/vg"; then
failed=$((failed + 1))
echo "MEMCHECK FAIL $f"
head -20 "$tmp/vg"
continue
fi
fi
pass=$((pass + 1))
done done
for f in fail/*.fn; do for f in fail/*.fn; do
expect=$(head -1 "$f" | sed -n 's|^// expect: ||p') expect=$(head -1 "$f" | sed -n 's|^// expect: ||p')
got=$("$FINCH" run "$f" 2>&1) got=$("$FINCH" run "$f" < /dev/null 2>&1)
status=$? status=$?
if [ $status -ne 0 ] && [[ "$got" == *"$expect"* ]]; then if [ $status -ne 0 ] && [[ "$got" == *"$expect"* ]]; then
pass=$((pass + 1)) pass=$((pass + 1))

17
tests/run/abi.c Normal file
View file

@ -0,0 +1,17 @@
#include "abi.h"
#include <string.h>
Vec2 vec2_add(Vec2 a, Vec2 b) { return (Vec2){a.x + b.x, a.y + b.y}; }
Vec3 vec3_scale(Vec3 v, float k) { return (Vec3){v.x * k, v.y * k, v.z * k}; }
Color color_invert(Color c) { return (Color){255 - c.r, 255 - c.g, 255 - c.b, c.a}; }
Mixed mixed_bump(Mixed m) { return (Mixed){m.d + 0.5, m.i + 1}; }
Pair pair_swap(Pair p) { return (Pair){p.b, p.a}; }
Big big_sum(Big a, Big b) { return (Big){a.a + b.a, a.b + b.b, a.c + b.c}; }
int64_t big_total(Big b) { return b.a + b.b + b.c; }
int named_len(Named n) { return (int)strlen(n.name) * 100 + n.count; }
Sprite sprite_move(Sprite s, float dx) { s.pos.x += dx; s.color.a = 7; return s; }
int item_sum(Item *it) { int s = 0; for (; it; it = it->next) s += it->value; return s; }
double many(Vec2 a, Vec2 b, Vec2 c, Vec2 d, Vec2 e, Vec2 f, Vec2 g, Vec2 h, Vec2 i, Pair p1, Pair p2, Pair p3, Pair p4) {
return a.x + b.x + c.x + d.x + e.x + f.x + g.x + h.x + i.x + i.y + p1.a + p2.b + p3.a + p4.b;
}
int64_t apply_pair(Pair (*fn)(Pair, int64_t), Pair p, int64_t k) { Pair r = fn(p, k); return r.a * 1000 + r.b; }
float apply_vec(Vec2 (*fn)(Vec2), Vec2 v) { Vec2 r = fn(v); return r.x + r.y; }

25
tests/run/abi.h Normal file
View file

@ -0,0 +1,25 @@
#include <stdint.h>
typedef struct { float x, y; } Vec2; // SSE: <2 x float>
typedef struct { float x, y, z; } Vec3; // SSE, SSE (float tail)
typedef struct { uint8_t r, g, b, a; } Color; // INTEGER: i32
typedef struct { double d; int i; } Mixed; // SSE + INTEGER
typedef struct { int64_t a, b; } Pair; // INTEGER, INTEGER
typedef struct { int64_t a, b, c; } Big; // MEMORY (24 bytes)
typedef struct { char name[8]; int count; } Named; // fixed array inside
typedef struct { Vec2 pos; Color color; } Sprite; // nested
typedef struct Item { int value; struct Item *next; } Item;
Vec2 vec2_add(Vec2 a, Vec2 b);
Vec3 vec3_scale(Vec3 v, float k);
Color color_invert(Color c);
Mixed mixed_bump(Mixed m);
Pair pair_swap(Pair p);
Big big_sum(Big a, Big b);
int64_t big_total(Big b);
int named_len(Named n);
Sprite sprite_move(Sprite s, float dx);
int item_sum(Item *it);
// many structs: runs out of registers, later ones go on the stack
double many(Vec2 a, Vec2 b, Vec2 c, Vec2 d, Vec2 e, Vec2 f, Vec2 g, Vec2 h, Vec2 i, Pair p1, Pair p2, Pair p3, Pair p4);
int64_t apply_pair(Pair (*fn)(Pair, int64_t), Pair p, int64_t k);
float apply_vec(Vec2 (*fn)(Vec2), Vec2 v);

34
tests/run/c_structs.fn Normal file
View file

@ -0,0 +1,34 @@
import "abi.h"
link "abi.c"
fn pairTimes(Pair p, int k) -> Pair {
return Pair(a: p.a * k, b: p.b + k)
}
fn flip(Vec2 v) -> Vec2 {
return Vec2(x: v.y, y: v.x * 10)
}
fn main() {
print(vec2_add(Vec2(1.5, 2), Vec2(x: 0.5, y: 3)))
print(vec3_scale(Vec3(1, 2, 3), 2))
print(color_invert(Color(r: 10, g: 20, b: 30, a: 255)))
print(mixed_bump(Mixed(d: 1.25, i: 41)))
print(pair_swap(Pair(1, 2)))
big := big_sum(Big(1, 2, 3), Big(10, 20, 30))
print(big, big_total(big))
Named n
n.name[0] = 'h'
n.name[1] = 'i'
n.count = 5
print(named_len(n), n.name.len)
s := sprite_move(Sprite(pos: Vec2(1, 1), color: Color(1, 2, 3, 4)), 2.5)
print(s)
v := Vec2(1, 0)
print(many(v, v, v, v, v, v, v, v, Vec2(2, 3), Pair(1, 0), Pair(0, 2), Pair(3, 0), Pair(0, 4)))
print(apply_pair(addr(pairTimes), Pair(2, 3), 5))
print(apply_vec(addr(flip), Vec2(1, 2)))
second := Item(value: 2, next: null)
first := Item(value: 1, next: addr(second))
print(item_sum(addr(first)))
}

12
tests/run/c_structs.out Normal file
View file

@ -0,0 +1,12 @@
Vec2(x: 2, y: 5)
Vec3(x: 2, y: 4, z: 6)
Color(r: 245, g: 235, b: 225, a: 255)
Mixed(d: 1.75, i: 42)
Pair(a: 2, b: 1)
Big(a: 11, b: 22, c: 33) 66
205 8
Sprite(pos: Vec2(x: 3.5, y: 1), color: Color(r: 1, g: 2, b: 3, a: 7))
23
10008
12
3

20
tests/run/defer.fn Normal file
View file

@ -0,0 +1,20 @@
fn work(int n) -> int {
defer print("cleanup", n)
if n > 1 {
return n * 10
}
defer print("only when small")
return n
}
fn main() {
print(work(1))
print(work(2))
for i in 0..3 {
defer print("end of round", i)
if i == 1 {
continue
}
print("round", i)
}
}

10
tests/run/defer.out Normal file
View file

@ -0,0 +1,10 @@
only when small
cleanup 1
1
cleanup 2
20
round 0
end of round 0
end of round 1
round 2
end of round 2

9
tests/run/files.fn Normal file
View file

@ -0,0 +1,9 @@
fn main() {
path := "/tmp/finch_test_file.txt"
ok := write_file(path, "line one\nline two\n")
print(ok, file_exists(path))
text := read_file(path)
lines := text.trim().split("\n")
print(lines, lines.len)
print(file_exists("/definitely/not/here"))
}

3
tests/run/files.out Normal file
View file

@ -0,0 +1,3 @@
true true
["line one", "line two"] 2
false

18
tests/run/geometry.fn Normal file
View file

@ -0,0 +1,18 @@
import "math.h"
struct Vec {
float x
float y
}
fn length(Vec v) -> float {
return sqrt(v.x * v.x + v.y * v.y)
}
fn add(Vec a, Vec b) -> Vec {
return Vec(a.x + b.x, a.y + b.y)
}
fn describe(Vec v) -> str {
return "(" + str(v.x) + ", " + str(v.y) + ") of length " + str(length(v))
}

7
tests/run/input.fn Normal file
View file

@ -0,0 +1,7 @@
fn main() {
name := input("Name? ")
age := int(input("Age? "))
print("Hi", name, "next year you will be", age + 1)
rest := input()
print("rest:", rest, rest.len)
}

2
tests/run/input.in Normal file
View file

@ -0,0 +1,2 @@
Ola
41

2
tests/run/input.out Normal file
View file

@ -0,0 +1,2 @@
Name? Age? Hi Ola next year you will be 42
rest: 0

98
tests/run/memory.fn Normal file
View file

@ -0,0 +1,98 @@
struct Node {
str name
ptr[Node] next
[]str tags
}
fn build(int n) -> []str {
[]str out
out.resize(n)
return out
}
fn firstLong([]str words) -> str {
for w in words {
tmp := w + "!"
if w.len > 3 {
return tmp
}
}
return "none"
}
fn loops() -> int {
count := 0
for i in 0..10 {
s := "item " + str(i)
if i == 2 {
continue
}
if i == 6 {
break
}
count += s.len
}
while true {
t := "x" + "y"
if t.len == 2 {
break
}
}
return count
}
fn deferred() -> str {
log := "start"
defer print("deferred ran, log was:", log)
log = log + " middle"
names := ["a", "b"]
defer print("names:", names)
return log + " end"
}
fn main([]str args) {
print(args.len > 0)
print(firstLong(["hi", "hello", "hey"]))
print(firstLong(["a"]))
print(loops())
print(deferred())
s := "ab"
s = s + s
s = s
print(s)
list := ["one", "two", "three"]
for w in list {
if w == "two" {
list.push("four")
}
}
print(list)
last := list.pop()
print(last, list)
head := new(Node(name: "first", tags: ["t1"]))
head.next = new(Node(name: "second"))
print(head.name, head.next.name, head.tags)
n := head
while n != null {
print("node", n.name)
n = n.next
}
free(head.next)
free(head)
[]str words
for i in 0..3 {
words.push("w" + str(i))
}
words.clear()
words.resize(2)
print(words, words.len)
print([1, 2, 3].slice(1, 3), "abc".bytes())
x := ["keep"]
y := x
x[0] = "changed"
print(x, y)
}

16
tests/run/memory.out Normal file
View file

@ -0,0 +1,16 @@
true
hello!
none
30
names: ["a", "b"]
deferred ran, log was: start middle
start middle end
abab
["one", "two", "three", "four"]
four ["one", "two", "three"]
first second ["t1"]
node first
node second
["", ""] 2
[2, 3] [97, 98, 99]
["changed"] ["keep"]

18
tests/run/modules.fn Normal file
View file

@ -0,0 +1,18 @@
import geometry
import shapes
fn main() {
v := geometry.Vec(3, 4)
print(geometry.length(v))
w := geometry.add(v, geometry.Vec(x: 1, y: 1))
print(geometry.describe(w))
geometry.Vec z
print(z)
b := shapes.Box(size: geometry.Vec(2, 5))
print(shapes.area(b), b)
total := 0
for i in 0..3 {
total += i
}
print("total", total)
}

5
tests/run/modules.out Normal file
View file

@ -0,0 +1,5 @@
5
(4, 5) of length 6.40312
Vec(x: 0, y: 0)
10 Box(corner: Vec(x: 0, y: 0), size: Vec(x: 2, y: 5))
total 3

View file

@ -0,0 +1,23 @@
struct Node {
int value
ptr[Node] next
}
struct Box {
int size
}
fn main() {
last := new(Node(value: 2, next: null))
first := Node(value: 1, next: last)
p := addr(first)
print(p.value, p.next.value)
p.value = 10
print(first.value)
b := Box(3)
q := addr(b)
print(q.value, q.size)
q.value = Box(7)
print(b)
free(last)
}

View file

@ -0,0 +1,4 @@
1 2
10
Box(size: 3) 3
Box(size: 7)

10
tests/run/shapes.fn Normal file
View file

@ -0,0 +1,10 @@
import geometry
struct Box {
geometry.Vec corner
geometry.Vec size
}
fn area(Box b) -> float {
return b.size.x * b.size.y
}

View file

@ -0,0 +1,78 @@
struct Point {
int x
int y
}
struct Player {
str name = "nobody"
Point pos
[]int scores
int lives = 3
}
fn moved(Point p, int dx) -> Point {
return Point(x: p.x + dx, y: p.y)
}
fn total([]int xs) -> int {
sum := 0
for x in xs {
sum += x
}
return sum
}
fn addScore(Player p, int s) -> Player {
p.scores.push(s)
return p
}
fn main() {
p := Point(1, 2)
print(p, p.x, p.y)
q := moved(p, 10)
print(q)
pl := Player(name: "Ola")
print(pl)
pl.scores.push(10)
pl.scores.push(20)
pl.pos.x = 5
print(pl.name, pl.lives, pl.scores, pl.pos)
copy := pl
copy.name = "Tom"
copy.scores.push(30)
print(pl.name, pl.scores.len, copy.name, copy.scores.len)
pl2 := addScore(pl, 99)
print(pl.scores, pl2.scores)
nums := [5, 3, 8, 1]
nums.sort()
print(nums, total(nums), nums.len)
print(nums.pop(), nums)
nums.insert(0, 42)
nums.remove(1)
print(nums, nums.contains(8), nums.find(3))
names := ["b", "a", "c"]
names.sort()
print(names, names.join("-"))
s := "Hello"
s += ", Finch"
print(s, s.len, s[0], s.upper(), s.sub(0, 5))
words := "a,b,,c".split(",")
print(words, words.len)
print(" trim ".trim(), "x".repeat(3), "abc".replace("b", "BB"), "hello".contains("ell"))
print(str(42) + "!", str(3.5), str(true), int("123") + 1, float("2.5") * 2)
s[0] = 'J'
print(s)
for c in "hi" {
print(c)
}
grid := [[1, 2], [3, 4]]
grid[1].push(5)
print(grid, grid[1][2])
}

View file

@ -0,0 +1,18 @@
Point(x: 1, y: 2) 1 2
Point(x: 11, y: 2)
Player(name: "Ola", pos: Point(x: 0, y: 0), scores: [], lives: 3)
Ola 3 [10, 20] Point(x: 5, y: 0)
Ola 2 Tom 3
[10, 20] [10, 20, 99]
[1, 3, 5, 8] 17 4
8 [1, 3, 5]
[42, 3, 5] false 1
["a", "b", "c"] a-b-c
Hello, Finch 12 H HELLO, FINCH Hello
["a", "b", "", "c"] 4
trim xxx aBBc true
42! 3.5 true 124 5
Jello, Finch
h
i
[[1, 2], [3, 4, 5]] 5