diff --git a/CMakeLists.txt b/CMakeLists.txt index 995b1dd..4d7401f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ 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_REQUIRED ON) @@ -13,9 +13,19 @@ add_executable(finch src/lexer.cpp src/parser.cpp src/codegen.cpp + src/codegen_types.cpp + src/codegen_builtins.cpp + src/abi.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"` find_path(LIBCLANG_INCLUDE clang-c/Index.h HINTS ${LLVM_INCLUDE_DIRS}) find_library(LIBCLANG NAMES clang libclang HINTS ${LLVM_LIBRARY_DIRS}) diff --git a/examples/guess.fn b/examples/guess.fn new file mode 100644 index 0000000..1fbb10a --- /dev/null +++ b/examples/guess.fn @@ -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 + } + } +} diff --git a/examples/structs.fn b/examples/structs.fn new file mode 100644 index 0000000..a3f96c2 --- /dev/null +++ b/examples/structs.fn @@ -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 + } +} diff --git a/examples/todo.fn b/examples/todo.fn new file mode 100644 index 0000000..21d37a1 --- /dev/null +++ b/examples/todo.fn @@ -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) +} diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c new file mode 100644 index 0000000..e3db36b --- /dev/null +++ b/runtime/finch_rt.c @@ -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 +#include +#include +#include +#include +#include + +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; +} diff --git a/shell.nix b/shell.nix index b4c65fe..d340bbc 100644 --- a/shell.nix +++ b/shell.nix @@ -2,10 +2,10 @@ pkgs.mkShell { packages = with pkgs; [ - # building Finch + # building Finch (the clang wrapper first, so `clang` finds the system headers) + llvmPackages.clang llvmPackages.llvm llvmPackages.libclang # reads C headers for `import "stdio.h"` - llvmPackages.clang cmake ninja pkg-config # finds the right flags for `link "..."` diff --git a/src/abi.cpp b/src/abi.cpp new file mode 100644 index 0000000..e44fd0f --- /dev/null +++ b/src/abi.cpp @@ -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 &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 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(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(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 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 ¶ms, bool variadic, + std::vector *extraTypes = nullptr) { + Plan pl; + int intRegs = 6, sseRegs = 8; + std::vector 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 ¶ms) { + 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(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 resolvedParams(Codegen &cg, const CFunc &f) { + std::vector 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 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 args, Pos p) { + FType ret = resolve(f.ret, p); + std::vector 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 ll; + Value *sret = nullptr; + if (pl.ret.kind == AbiArg::Memory) { + sret = tmpOf(ty(ret)); + cast(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(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 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 diff --git a/src/ast.h b/src/ast.h index c779cb2..6f72215 100644 --- a/src/ast.h +++ b/src/ast.h @@ -4,10 +4,23 @@ #include #include +struct StructInfo; // filled in by codegen: fields, LLVM type, ownership + 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; - std::shared_ptr elem; // what a Ptr points to; null for the untyped `ptr` + std::shared_ptr 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(Kind k) : kind(k) {} @@ -16,6 +29,11 @@ struct Type { p.elem = std::make_shared(t); return p; } + static Type arrayOf(const Type &t) { + Type a(Array); + a.elem = std::make_shared(t); + return a; + } bool isInt() const { return kind >= I8 && kind <= U64; } bool isSigned() const { return kind >= I8 && kind <= I64; } @@ -35,13 +53,21 @@ struct Type { bool operator==(const Type &o) const { if (kind != o.kind) return false; - if (kind != Ptr) return true; - if (!elem || !o.elem) return !elem && !o.elem; - return *elem == *o.elem; + switch (kind) { + case Ptr: + 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); } - std::string name() const { + // How the type is written in Finch, for error messages. + std::string show() const { switch (kind) { case Void: return "nothing"; case Bool: return "bool"; @@ -57,8 +83,12 @@ struct Type { case F32: return "f32"; case F64: return "float"; 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 Array: return "[]" + elem->show(); + case Fixed: return "[" + std::to_string(count) + "]" + elem->show(); + case Struct: + case Named: return module.empty() ? name : module + "." + name; } return "?"; } @@ -69,11 +99,12 @@ bool typeFromName(const std::string &name, Type &out); struct Pos { int line = 0, col = 0; + int file = 0; // index into the list of source files (see error.h) }; // ---------- 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 { ExprKind kind; @@ -127,21 +158,39 @@ struct BinaryExpr : Expr { BinaryExpr(Pos p, BinOp o, ExprPtr l, ExprPtr 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 { std::string callee; std::vector args; + std::vector argNames; // "" for positional; Point(x: 1, y: 2) 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; std::string field; MemberExpr(Pos p, ExprPtr o, std::string 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 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 args; + std::vector argNames; + MethodExpr(Pos p, ExprPtr o, std::string n) : Expr(ExprKind::Method, p), obj(std::move(o)), name(std::move(n)) {} +}; // ---------- 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 { StmtKind kind; @@ -165,7 +214,7 @@ struct VarDeclStmt : Stmt { VarDeclStmt(Pos p) : Stmt(StmtKind::VarDecl, p) {} }; struct AssignStmt : Stmt { - ExprPtr target; // a variable or p.value + ExprPtr target; // a variable, field, element or p.value char op; // '=', '+', '-', '*', '/', '%' ExprPtr value; AssignStmt(Pos p) : Stmt(StmtKind::Assign, p) {} @@ -191,6 +240,12 @@ struct ForStmt : Stmt { BlockPtr body; 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 { ExprPtr value; // may be null ReturnStmt(Pos p) : Stmt(StmtKind::Return, p) {} @@ -201,6 +256,10 @@ struct BreakStmt : Stmt { struct ContinueStmt : Stmt { explicit ContinueStmt(Pos p) : Stmt(StmtKind::Continue, p) {} }; +struct DeferStmt : Stmt { // defer : runs when the block ends + StmtPtr body; + DeferStmt(Pos p, StmtPtr s) : Stmt(StmtKind::Defer, p), body(std::move(s)) {} +}; // ---------- top level ---------- @@ -218,18 +277,36 @@ struct FnDecl { 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 fields; +}; + struct Import { 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 { 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 { + std::string module; // "" for the main file, else the module's name + std::string path; std::vector imports; std::vector links; + std::vector structs; std::vector fns; }; diff --git a/src/cimport.cpp b/src/cimport.cpp index 99fa255..1123afe 100644 --- a/src/cimport.cpp +++ b/src/cimport.cpp @@ -41,189 +41,300 @@ std::string str(CXString s) { return out; } -// C type -> Finch type. Returns false (with a reason) if Finch can't express it yet. -bool mapType(CXType t, Type &out, std::string &why) { - t = clang_getCanonicalType(t); - 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; - }; +// Where a C type is used decides how `char *` looks in Finch: in function signatures it is +// a `str`; inside structs, globals and behind pointers it stays a raw `ptr[char]`. +enum class Ctx { Signature, Data }; - switch (t.kind) { - case CXType_Void: out = Type::Void; return true; - case CXType_Bool: out = Type::Bool; return true; - case CXType_Char_S: - case CXType_Char_U: out = 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: out = Type::F32; return true; - case CXType_Double: out = 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) { - out = Type::Str; // char* is a C string +struct Importer { + CImports &out; + CXTranslationUnit tu = nullptr; + std::string header; + std::map recordNames; // record USR -> Finch name (typedef wins) + std::map defining; // USR -> 1 while its fields are being mapped + int anon = 0; + + explicit Importer(CImports &o) : out(o) {} + + // The Finch name of a C record: its typedef name, its tag, or a made-up one. + std::string recordName(CXType t) { + CXCursor decl = clang_getTypeDeclaration(t); + std::string usr = str(clang_getCursorUSR(decl)); + auto it = recordNames.find(usr); + if (it != recordNames.end()) return it->second; + std::string name = str(clang_getCursorSpelling(decl)); + if (name.empty() || name.find(' ') != std::string::npos) name = "anon_struct_" + std::to_string(++anon); + recordNames[usr] = name; + return name; + } + + // 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(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; } - Type inner; - std::string ignored; - // pointers to structs, functions, void...: an opaque `ptr` you can pass around - if (pointee.kind == CXType_Void || !mapType(pointee, inner, ignored) || inner.kind == Type::Void) - out = Type::Ptr; - else - out = Type::ptrTo(inner); + case CXType_Record: { + if (clang_getCursorKind(clang_getTypeDeclaration(t)) == CXCursor_UnionDecl) { + why = "uses a C union by value"; + return false; + } + std::string name = defineRecord(t); + 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(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 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; } - 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. -bool macroValue(CXTranslationUnit tu, CXCursor c, CConst &out) { - CXToken *toks; - unsigned n; - clang_tokenize(tu, clang_getCursorExtent(c), &toks, &n); - std::vector 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]))); + void visitTop(CXCursor c) { + std::string name = str(clang_getCursorSpelling(c)); + switch (clang_getCursorKind(c)) { + case CXCursor_TypedefDecl: { + // typedef struct {...} Name; -> the struct is called Name in Finch + CXType u = clang_getCanonicalType(clang_getTypedefDeclUnderlyingType(c)); + if (u.kind == CXType_Record) { + std::string usr = str(clang_getCursorUSR(clang_getTypeDeclaration(u))); + // 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(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(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 -CImports importHeaders(const std::vector &imports, const std::string &dir) { +CImports importHeaders(const std::vector &imports, const std::vector &dirs) { CImports out; - if (imports.empty()) return out; + bool any = false; + for (const Import &im : imports) any |= im.isC; + if (!any) return out; - std::vector args = {"-x", "c", "-std=gnu11", "-I" + dir}; + std::vector 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); std::vector argv; for (const std::string &a : args) argv.push_back(a.c_str()); + Importer imp(out); CXIndex index = clang_createIndex(0, 0); for (const Import &im : imports) { + if (!im.isC) continue; std::string src = "#include \"" + im.path + "\"\n"; CXUnsavedFile file = {"finch_import.c", src.c_str(), (unsigned long)src.size()}; CXTranslationUnit tu = nullptr; CXErrorCode err = clang_parseTranslationUnit2( index, "finch_import.c", argv.data(), (int)argv.size(), &file, 1, 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++) { CXDiagnostic d = clang_getDiagnostic(tu, k); @@ -231,14 +342,18 @@ CImports importHeaders(const std::vector &imports, const std::string &di std::string msg = str(clang_getDiagnosticSpelling(d)); clang_disposeDiagnostic(d); if (msg.find("file not found") != std::string::npos) - fail(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, "can't find the C header '" + im.path + "'"); + failAt(im.pos.file, im.pos.line, im.pos.col, "C header '" + im.path + "' has an error: " + msg); } clang_disposeDiagnostic(d); } - Visit v{&out, tu, im.path}; - clang_visitChildren(clang_getTranslationUnitCursor(tu), visit, &v); + imp.tu = tu; + imp.header = im.path; + clang_visitChildren(clang_getTranslationUnitCursor(tu), [](CXCursor c, CXCursor, CXClientData d) { + static_cast(d)->visitTop(c); + return CXChildVisit_Continue; + }, &imp); clang_disposeTranslationUnit(tu); } clang_disposeIndex(index); diff --git a/src/cimport.h b/src/cimport.h index d977d6d..ecc3605 100644 --- a/src/cimport.h +++ b/src/cimport.h @@ -2,11 +2,13 @@ #include "ast.h" +#include #include #include #include // What Finch learned from `import "something.h"`. +// C structs by value appear in types as Named{name, module "C"}. struct CFunc { std::string name; @@ -27,11 +29,28 @@ struct CGlobal { // extern variables like stdout 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 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 { std::unordered_map fns; std::unordered_map consts; std::unordered_map globals; + std::map structs; }; -// `dir` is the folder of the .fn file, searched first for "local.h" headers. -CImports importHeaders(const std::vector &imports, const std::string &dir); +// `dirs`: folders searched first for "local.h" headers (the .fn files' folders). +CImports importHeaders(const std::vector &imports, const std::vector &dirs); diff --git a/src/codegen.cpp b/src/codegen.cpp index 38ea2c7..01df567 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -1,887 +1,1090 @@ -#include "codegen.h" -#include "error.h" +// Code generation, part 1: the program, statements, scopes and expressions. +// Types, ownership and printing live in codegen_types.cpp; calls and built-ins in +// codegen_builtins.cpp; the C calling convention in abi.cpp. -#include -#include -#include -#include - -#include +#include "codegen_impl.h" using namespace llvm; -using FType = ::Type; // Finch types (llvm::Type is the LLVM one) -namespace { +namespace finch { -struct Value_ { - Value *v; - FType type; - bool literal = false; // a number written in the code (or a C constant): adapts to the type around it -}; +Codegen::Codegen(LLVMContext &c, const std::vector &ps, const CImports &ci, TargetMachine &tm, bool dbg) + : ctx(c), b(c), mod(std::make_unique("finch", c)), progs(ps), cimports(ci), debug(dbg) { + mod->setTargetTriple(tm.getTargetTriple()); + mod->setDataLayout(tm.createDataLayout()); + dl = &mod->getDataLayout(); + scopes.reserve(256); + if (debug) { + di = std::make_unique(*mod); + for (const SourceFile &f : g_files) { + std::string path = f.path; + size_t slash = path.find_last_of('/'); + std::string dir = slash == std::string::npos ? "." : path.substr(0, slash); + std::string name = slash == std::string::npos ? path : path.substr(slash + 1); + diFiles.push_back(di->createFile(name, dir)); + } + diUnit = di->createCompileUnit(dwarf::DW_LANG_C, diFiles[0], "finch " FINCH_VERSION, false, "", 0); + mod->addModuleFlag(Module::Warning, "Debug Info Version", DEBUG_METADATA_VERSION); + mod->addModuleFlag(Module::Warning, "Dwarf Version", 5); + } +} -struct Var { - AllocaInst *slot; - FType type; - bool readonly; // loop variables -}; +std::unique_ptr Codegen::run() { + declareStructs(); + declareFns(); + if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')"); + for (auto &[name, m] : modules) + for (auto &[fname, fn] : m.fns) define(fn); + defineMainWrapper(); + if (di) di->finalize(); -struct Fn { - Function *llvm; - const FnDecl *decl; -}; + std::string err; + raw_string_ostream os(err); + if (verifyModule(*mod, &os)) { + std::fprintf(stderr, "internal compiler error (please report):\n%s\n", err.c_str()); + std::exit(2); + } + return std::move(mod); +} -struct Loop { - BasicBlock *continueTo, *breakTo; -}; +// ---------- declarations ---------- -struct Place { // something that can be assigned to: a variable or p.value +void Codegen::declareStructs() { + for (const Program &p : progs) { + ModuleScope &m = modules[p.module]; + for (const Import &im : p.imports) + if (!im.isC) m.imports.insert(im.path); + for (const StructDecl &d : p.structs) { + if (m.structs.count(d.name)) failAt(d.pos.file, d.pos.line, d.pos.col, "the struct '" + d.name + "' is defined twice"); + auto s = std::make_unique(); + s->name = d.name; + s->module = p.module; + s->decl = &d; + s->pos = d.pos; + s->llvm = StructType::create(ctx, "finch." + (p.module.empty() ? "" : p.module + ".") + d.name); + m.structs[d.name] = s.get(); + structStore.push_back(std::move(s)); + } + } + for (const auto &[name, cs] : cimports.structs) { + auto s = std::make_unique(); + s->name = name; + s->module = "C"; + s->isC = true; + s->cdecl = &cs; + s->unsupported = cs.unsupported; + s->llvm = StructType::create(ctx, "c." + name); + cStructs[name] = s.get(); + structStore.push_back(std::move(s)); + } + for (auto &s : structStore) resolveStruct(s.get()); +} + +void Codegen::declareFns() { + for (const Program &p : progs) + for (const FnDecl &fn : p.fns) declare(p, fn); +} + +void Codegen::declare(const Program &p, const FnDecl &fn) { + ModuleScope &m = modules[p.module]; + Pos pos = fn.pos; + if (m.fns.count(fn.name)) failAt(pos.file, pos.line, pos.col, "function '" + fn.name + "' is defined twice"); + if (isBuiltin(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is a built-in function, pick another name"); + if (m.structs.count(fn.name)) failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is already the name of a struct"); + + curModule = p.module; + Fn f; + f.decl = &fn; + f.module = p.module; + f.ret = resolve(fn.ret, pos); + for (const Param &prm : fn.params) f.params.push_back(resolve(prm.type, prm.pos)); + + bool isMain = p.module.empty() && fn.name == "main"; + if (isMain) { + bool argsOk = f.params.empty() || (f.params.size() == 1 && f.params[0] == FType::arrayOf(FType::Str)); + if (!argsOk) failAt(pos.file, pos.line, pos.col, "main takes no parameters, or the program's arguments: fn main([]str args)"); + FType::Kind r = f.ret.kind; + if (r != FType::Void && r != FType::I64 && r != FType::I32) + failAt(pos.file, pos.line, pos.col, "main returns nothing (fn main()) or an exit code (fn main() -> int)"); + } + + std::vector params; + for (size_t i = 0; i < f.params.size(); i++) { + params.push_back(ty(f.params[i])); + f.borrowParam.push_back(owning(f.params[i]) && !mutates(fn.params[i].name, *fn.body)); + } + // User functions get a prefix so they never clash with C library names. + std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + fn.name; + f.llvm = Function::Create(FunctionType::get(ty(f.ret), params, false), Function::InternalLinkage, name, mod.get()); + m.fns[fn.name] = f; +} + +// The real C `main` calls the Finch one and turns its result into an exit code. +void Codegen::defineMainWrapper() { + Fn &fm = modules[""].fns["main"]; + Function *w = Function::Create(FunctionType::get(b.getInt32Ty(), {b.getInt32Ty(), b.getPtrTy()}, false), + Function::ExternalLinkage, "main", mod.get()); + b.SetInsertPoint(BasicBlock::Create(ctx, "entry", w)); + b.SetCurrentDebugLocation(DebugLoc()); + std::vector args; + Value *argsArr = nullptr; + if (!fm.params.empty()) { + Value *out = tmp(Constant::getNullValue(ty(fm.params[0]))); + b.CreateCall(rt("finch_args"), {out, w->getArg(0), w->getArg(1)}); + argsArr = b.CreateLoad(ty(fm.params[0]), out); + args.push_back(argsArr); + } + Value *r = b.CreateCall(fm.llvm, args); + if (argsArr) dropValue(argsArr, fm.params[0]); + if (fm.ret.kind == FType::Void) b.CreateRet(b.getInt32(0)); + else b.CreateRet(b.CreateIntCast(r, b.getInt32Ty(), true)); +} + +void Codegen::define(Fn &fn) { + const FnDecl &d = *fn.decl; + curFn = &fn; + curModule = fn.module; + Function *f = fn.llvm; + b.SetInsertPoint(BasicBlock::Create(ctx, "entry", f)); + if (di) { + DIFile *file = diFiles[d.pos.file]; + std::vector types{diType(fn.ret)}; + for (const FType &t : fn.params) types.push_back(diType(t)); + diFn = di->createFunction(file, d.name, f->getName(), file, d.pos.line, + di->createSubroutineType(di->getOrCreateTypeArray(types)), d.pos.line, + DINode::FlagPrototyped, DISubprogram::SPFlagDefinition); + f->setSubprogram(diFn); + } + setLoc(d.pos); + + scopes.clear(); + varOrder = 0; + pushScope(); + unsigned i = 0; + for (Argument &arg : f->args()) { + const Param &p = d.params[i]; + const FType &t = fn.params[i]; + arg.setName(p.name); + if (owning(t) && !fn.borrowParam[i]) addVar(p.pos, p.name, t, copyValue(&arg, t), true, false, i + 1); // changed inside: own a copy + else addVar(p.pos, p.name, t, &arg, false, false, i + 1); + i++; + } + blockBody(*d.body); + if (!terminated()) { + if (fn.ret.kind != FType::Void) + failAt(d.pos.file, d.pos.line, d.pos.col, "function '" + d.name + "' can reach its end without returning " + fn.ret.show()); + emitCleanups(0); + b.CreateRetVoid(); + } + scopes.clear(); + diFn = nullptr; +} + +// ---------- scopes ---------- + +void Codegen::pushScope() { scopes.emplace_back(); } + +void Codegen::popScope() { + if (!terminated()) emitScopeCleanups(scopes.size() - 1); + scopes.pop_back(); +} + +void Codegen::emitCleanups(size_t downTo) { + for (size_t i = scopes.size(); i-- > downTo;) emitScopeCleanups(i); +} + +// Drop the scope's variables and run its defers, newest first. +void Codegen::emitScopeCleanups(size_t idx) { + for (size_t k = scopes[idx].cleanups.size(); k-- > 0;) { + Cleanup c = scopes[idx].cleanups[k]; // a copy: emitting a defer may grow `scopes` + if (!c.isDefer) { + Var v = scopes[idx].vars.at(c.var); + if (!v.moved) dropAt(v.slot, v.type); + continue; + } + int savedLimit = deferLimit; + deferLimit = c.order; + inDefer++; + pushScope(); + stmt(*c.body); + popScope(); + inDefer--; + deferLimit = savedLimit; + } +} + +bool Codegen::terminated() { return b.GetInsertBlock()->getTerminator() != nullptr; } + +BasicBlock *Codegen::newBlock(const char *name) { + return BasicBlock::Create(ctx, name, b.GetInsertBlock()->getParent()); +} + +// Variables live in stack slots created at the top of the function; LLVM turns them into registers. +AllocaInst *Codegen::slot(const FType &t, const std::string &name) { + Function *f = b.GetInsertBlock()->getParent(); + IRBuilder<> entry(&f->getEntryBlock(), f->getEntryBlock().begin()); + AllocaInst *a = entry.CreateAlloca(ty(t), nullptr, name); + if (t.kind == FType::Struct && t.info->isC) a->setAlignment(Align(t.info->cdecl->align)); + return a; +} + +Var *Codegen::lookup(const std::string &name) { + for (size_t i = scopes.size(); i-- > 0;) { + auto found = scopes[i].vars.find(name); + if (found == scopes[i].vars.end()) continue; + if (deferLimit >= 0 && found->second.order > deferLimit) continue; // declared after the defer + return &found->second; + } + return nullptr; +} + +Var &Codegen::addVar(Pos p, const std::string &name, const FType &t, Value *init, bool owned, bool readonly, + unsigned argNo) { + if (lookup(name)) failAt(p.file, p.line, p.col, "a variable named '" + name + "' already exists here"); + ModuleScope &m = modules[curModule]; + if (m.fns.count(name)) failAt(p.file, p.line, p.col, "'" + name + "' is already the name of a function"); + if (m.imports.count(name)) failAt(p.file, p.line, p.col, "'" + name + "' is already the name of an imported module"); + if (isBuiltin(name)) failAt(p.file, p.line, p.col, "'" + name + "' is a built-in name, pick another one"); + AllocaInst *s = slot(t, name); + b.CreateStore(init, s); + Var v; + v.slot = s; + v.type = t; + v.readonly = readonly; + v.owned = owned && owning(t); + v.order = ++varOrder; + if (v.owned) scopes.back().cleanups.push_back({false, name}); + if (di && name[0] != '$') { + DILocalVariable *dv = argNo ? di->createParameterVariable(diFn, name, argNo, diFiles[p.file], p.line, diType(t)) + : di->createAutoVariable(diFn, name, diFiles[p.file], p.line, diType(t)); + di->insertDeclare(s, dv, di->createExpression(), DILocation::get(ctx, p.line, p.col, diFn), b.GetInsertBlock()); + } + return scopes.back().vars[name] = v; +} + +// ---------- statements ---------- + +void Codegen::block(const BlockStmt &blk) { + pushScope(); + blockBody(blk); + popScope(); +} + +void Codegen::blockBody(const BlockStmt &blk) { + for (const StmtPtr &s : blk.body) { + if (terminated()) + failAt(s->pos.file, s->pos.line, s->pos.col, "this code can never run (it comes after return, break or continue)"); + stmt(*s); + } +} + +void Codegen::stmt(const Stmt &s) { + setLoc(s.pos); + switch (s.kind) { + case StmtKind::Block: block(static_cast(s)); break; + case StmtKind::VarDecl: varDecl(static_cast(s)); break; + case StmtKind::Assign: assign(static_cast(s)); break; + case StmtKind::Expr: release(expr(*static_cast(s).expr)); break; + case StmtKind::If: ifStmt(static_cast(s)); break; + case StmtKind::While: whileStmt(static_cast(s)); break; + case StmtKind::For: forStmt(static_cast(s)); break; + case StmtKind::ForEach: forEachStmt(static_cast(s)); break; + case StmtKind::Return: returnStmt(static_cast(s)); break; + case StmtKind::Break: jump(true, s.pos); break; + case StmtKind::Continue: jump(false, s.pos); break; + case StmtKind::Defer: { + auto &d = static_cast(s); + scopes.back().cleanups.push_back({true, "", d.body.get(), varOrder}); + break; + } + } +} + +void Codegen::jump(bool isBreak, Pos p) { + const char *word = isBreak ? "break" : "continue"; + if (inDefer) failAt(p.file, p.line, p.col, std::string(word) + " can't be used inside defer"); + if (loops.empty()) failAt(p.file, p.line, p.col, std::string(word) + " can only be used inside a loop"); + Loop l = loops.back(); + emitCleanups(l.scopeDepth); + b.CreateBr(isBreak ? l.breakTo : l.continueTo); +} + +void Codegen::varDecl(const VarDeclStmt &s) { + Pos p = s.pos; + if (!s.hasType) { + Value_ v = expr(*s.init); + Pos ip = s.init->pos; + if (v.type.kind == FType::Void) failAt(ip.file, ip.line, ip.col, "this gives back nothing, so it can't be stored"); + if (v.type.kind == FType::Null) failAt(ip.file, ip.line, ip.col, "null needs a type, like: ptr[int] " + s.name + " = null"); + addVar(p, s.name, v.type, own(v), true); + return; + } + FType t = resolve(s.type, p); + if (t.kind == FType::Void) failAt(p.file, p.line, p.col, "a variable can't have the type 'nothing'"); + Value_ v = s.init ? coerce(exprWant(*s.init, t), t, s.init->pos, "'" + s.name + "'") : defaultValue(t, p); + addVar(p, s.name, t, own(v), true); +} + +void Codegen::assign(const AssignStmt &s) { + std::string root = rootVar(*s.target); + if (!root.empty()) { + Var *v = lookup(root); + if (v && v->readonly) failAt(s.pos.file, s.pos.line, s.pos.col, v->readonlyWhy); + if (!v && s.target->kind == ExprKind::Var && !cimports.globals.count(root)) + failAt(s.pos.file, s.pos.line, s.pos.col, "there is no variable named '" + root + "' (create it with " + root + " := ...)"); + } + std::string what = s.target->kind == ExprKind::Var ? "'" + root + "'" : "the value"; + + if (s.op == '=') { + // The right side first: it may move memory the target lives in (a.push(...) inside it). + Value_ v; + bool known = false; + FType want; + if (s.target->kind == ExprKind::Var) { + if (Var *var = lookup(root)) { want = var->type; known = true; } + } + v = known ? exprWant(*s.value, want) : expr(*s.value); + Place dst = place(*s.target, true); + v = coerce(v, dst.type, s.value->pos, what); + Value *nv = own(v); + if (owning(dst.type)) { + Value *old = b.CreateLoad(ty(dst.type), dst.addr); + b.CreateStore(nv, dst.addr); + dropValue(old, dst.type); + } else { + b.CreateStore(nv, dst.addr); + } + return; + } + BinOp op = s.op == '+' ? BinOp::Add : s.op == '-' ? BinOp::Sub : s.op == '*' ? BinOp::Mul + : s.op == '/' ? BinOp::Div : BinOp::Mod; + Value_ rhs = expr(*s.value); + Place dst = place(*s.target, true); + Value_ cur{b.CreateLoad(ty(dst.type), dst.addr), dst.type}; + Value_ v = coerce(arith(op, cur, rhs, s.pos), dst.type, s.pos, what); + Value *nv = own(v); + if (owning(dst.type)) { + b.CreateStore(nv, dst.addr); + dropValue(cur.v, dst.type); + } else { + b.CreateStore(nv, dst.addr); + } +} + +Value *Codegen::condition(const Expr &e) { return coerce(expr(e), FType::Bool, e.pos, "a condition").v; } + +void Codegen::ifStmt(const IfStmt &s) { + Value *cond = condition(*s.cond); + BasicBlock *thenBB = newBlock("then"); + BasicBlock *elseBB = s.otherwise ? newBlock("else") : nullptr; + BasicBlock *end = newBlock("endif"); + b.CreateCondBr(cond, thenBB, elseBB ? elseBB : end); + + b.SetInsertPoint(thenBB); + block(*s.then); + BasicBlock *last = b.GetInsertBlock(); + if (!terminated()) b.CreateBr(end); + + if (elseBB) { + b.SetInsertPoint(elseBB); + stmt(*s.otherwise); + last = b.GetInsertBlock(); + if (!terminated()) b.CreateBr(end); + } + continueAt(end, last); +} + +// Continue emitting into `bb`; if nothing jumps there, drop it and stay "after a return". +void Codegen::continueAt(BasicBlock *bb, BasicBlock *deadEnd) { + if (pred_empty(bb)) { + bb->eraseFromParent(); + b.SetInsertPoint(deadEnd); + } else { + b.SetInsertPoint(bb); + } +} + +void Codegen::whileStmt(const WhileStmt &s) { + BasicBlock *condBB = newBlock("while.cond"); + BasicBlock *body = newBlock("while.body"); + BasicBlock *end = newBlock("while.end"); + b.CreateBr(condBB); + + b.SetInsertPoint(condBB); + Value *cond = condition(*s.cond); + if (auto *c = dyn_cast(cond); c && c->isOne()) b.CreateBr(body); // while true + else b.CreateCondBr(cond, body, end); + + b.SetInsertPoint(body); + loops.push_back({condBB, end, scopes.size()}); + block(*s.body); + loops.pop_back(); + if (!terminated()) b.CreateBr(condBB); + continueAt(end, b.GetInsertBlock()); +} + +void Codegen::forStmt(const ForStmt &s) { + Value *from = coerce(expr(*s.from), FType::I64, s.from->pos, "the start of a range").v; + Value *to = coerce(expr(*s.to), FType::I64, s.to->pos, "the end of a range").v; + + pushScope(); + Var &iv = addVar(s.pos, s.var, FType::I64, from, false, true); + iv.readonlyWhy = "the loop variable '" + s.var + "' can't be changed"; + Value *i = iv.slot; + + BasicBlock *condBB = newBlock("for.cond"); + BasicBlock *body = newBlock("for.body"); + BasicBlock *step = newBlock("for.step"); + BasicBlock *end = newBlock("for.end"); + b.CreateBr(condBB); + + b.SetInsertPoint(condBB); + b.CreateCondBr(b.CreateICmpSLT(b.CreateLoad(b.getInt64Ty(), i), to), body, end); + + b.SetInsertPoint(body); + loops.push_back({step, end, scopes.size()}); + block(*s.body); + loops.pop_back(); + if (!terminated()) b.CreateBr(step); + + b.SetInsertPoint(step); + b.CreateStore(b.CreateAdd(b.CreateLoad(b.getInt64Ty(), i), b.getInt64(1)), i); + b.CreateBr(condBB); + + b.SetInsertPoint(end); + popScope(); +} + +// for x in list: x is each element (or each char of a str) +void Codegen::forEachStmt(const ForEachStmt &s) { + pushScope(); + LRef r = ref(*s.list); Value *addr; - FType type; -}; - -class Codegen { -public: - Codegen(LLVMContext &c, const CImports &ci, TargetMachine &tm) - : ctx(c), b(c), mod(std::make_unique("finch", c)), cimports(ci) { - mod->setTargetTriple(tm.getTargetTriple()); - mod->setDataLayout(tm.createDataLayout()); + FType lt; + bool live = r.isPlace; // iterate the variable itself: re-read its length every round + if (r.isPlace) { + addr = r.pl.addr; + lt = r.pl.type; + } else { + lt = r.val.type; + addr = addVar(s.pos, "$list" + std::to_string(varOrder), lt, own(r.val), true).slot; } + if (lt.kind != FType::Array && lt.kind != FType::Str && lt.kind != FType::Fixed) + failAt(s.list->pos.file, s.list->pos.line, s.list->pos.col, + "for ... in needs an array or a str, but this is " + lt.show() + " (for a range of numbers write 0..n)"); + FType et = lt.kind == FType::Str ? FType(FType::Char) : *lt.elem; + // If the loop changes the list, each element is copied, so it can't disappear under us. + bool copyElems = live && owning(et) && mutates(rootVar(*s.list), *s.body); - std::unique_ptr run(const Program &prog) { - // Declare every function first, so order in the file doesn't matter. - for (const FnDecl &fn : prog.fns) declare(fn); - if (!fns.count("main")) fail(1, 1, "there is no main function (add 'fn main() { ... }')"); - for (const FnDecl &fn : prog.fns) define(fn); + Value *i = slot(FType::I64, "$i"); + b.CreateStore(b.getInt64(0), i); + BasicBlock *condBB = newBlock("each.cond"); + BasicBlock *body = newBlock("each.body"); + BasicBlock *step = newBlock("each.step"); + BasicBlock *end = newBlock("each.end"); + b.CreateBr(condBB); - std::string err; - raw_string_ostream os(err); - if (verifyModule(*mod, &os)) { - std::fprintf(stderr, "internal compiler error (please report):\n%s\n", err.c_str()); - std::exit(2); - } - return std::move(mod); + b.SetInsertPoint(condBB); + Value *len = lt.kind == FType::Fixed ? (Value *)b.getInt64(lt.count) : b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(lt), addr, 1)); + Value *iv = b.CreateLoad(b.getInt64Ty(), i); + b.CreateCondBr(b.CreateICmpSLT(iv, len), body, end); + + b.SetInsertPoint(body); + Value *elemAddr; + if (lt.kind == FType::Fixed) { + elemAddr = b.CreateInBoundsGEP(ty(lt), addr, {b.getInt64(0), iv}); + } else { + Value *data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(lt), addr, 0)); + elemAddr = b.CreateInBoundsGEP(ty(et), data, iv); } + Value *elem = b.CreateLoad(ty(et), elemAddr); + loops.push_back({step, end, scopes.size()}); + pushScope(); + Var &xv = addVar(s.pos, s.var, et, copyElems ? copyValue(elem, et) : elem, copyElems, true); + xv.readonlyWhy = "the loop variable '" + s.var + "' can't be changed (to change the list, loop with for i in 0..list.len and use list[i])"; + block(*s.body); + popScope(); + loops.pop_back(); + if (!terminated()) b.CreateBr(step); -private: - LLVMContext &ctx; - IRBuilder<> b; - std::unique_ptr mod; - const CImports &cimports; + b.SetInsertPoint(step); + b.CreateStore(b.CreateAdd(b.CreateLoad(b.getInt64Ty(), i), b.getInt64(1)), i); + b.CreateBr(condBB); - std::unordered_map fns; - std::vector> scopes; - std::vector loops; - const FnDecl *curFn = nullptr; + b.SetInsertPoint(end); + popScope(); +} - // ---------- types ---------- - - llvm::Type *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::Ptr: - case FType::Null: return b.getPtrTy(); - } - return nullptr; +void Codegen::returnStmt(const ReturnStmt &s) { + const FnDecl &d = *curFn->decl; + if (inDefer) failAt(s.pos.file, s.pos.line, s.pos.col, "return can't be used inside defer"); + if (curFn->ret.kind == FType::Void) { + if (s.value) + failAt(s.value->pos.file, s.value->pos.line, s.value->pos.col, "'" + d.name + "' doesn't return a value (it has no '-> type')"); + emitCleanups(0); + b.CreateRetVoid(); + return; } + if (!s.value) failAt(s.pos.file, s.pos.line, s.pos.col, "'" + d.name + "' must return " + curFn->ret.show()); - Constant *zero(const FType &t) { - if (t.kind == FType::Str) return cast(b.CreateGlobalString("", "empty")); - return Constant::getNullValue(ty(t)); - } - - // Can every value of `from` be stored in `to`? (i8 -> i32 yes, i32 -> u32 no) - static bool widens(const FType &from, const FType &to) { - if (!from.isInt() || !to.isInt()) return false; - if (from.isSigned() && to.isUnsigned()) return false; - if (from.isSigned() == to.isSigned()) return to.bits() > from.bits(); - return to.bits() > from.bits(); // unsigned -> bigger signed - } - - // Does the constant number `c` (of type `from`) fit into `to`? - static bool 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 *intCast(Value *v, const FType &from, const FType &to) { - return b.CreateIntCast(v, ty(to), from.isSigned()); - } - - Value *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 *coerce(Value_ v, const FType &want, Pos p, const std::string &what) { - const FType &from = v.type; - if (from == want) return v.v; - if (from.kind == FType::Null && (want.isPtr() || want.kind == FType::Str)) - return ConstantPointerNull::get(b.getPtrTy()); - // any pointer (or str) fits an untyped `ptr`; an untyped ptr fits any pointer - if (want.isPtr() && !want.elem && (from.isPtr() || from.kind == FType::Str)) return v.v; - if (from.isPtr() && !from.elem && want.isPtr()) return v.v; - - if (from.isInt() && want.isInt()) { - auto *c = v.literal ? dyn_cast(v.v) : nullptr; - if ((c && fits(c, from, want)) || widens(from, want)) return intCast(v.v, from, want); - if (c) fail(p.line, p.col, "the number " + std::to_string(from.isSigned() ? c->getSExtValue() : (long long)c->getZExtValue()) + - " doesn't fit in " + want.name()); - } - if (from.isInt() && want.isFloat()) return intToFloat(v.v, from, want); - if (from.kind == FType::F32 && want.kind == FType::F64) return b.CreateFPExt(v.v, ty(want)); - if (from.kind == FType::F64 && want.kind == FType::F32 && v.literal) - return b.CreateFPTrunc(v.v, ty(want)); - - std::string msg = what + " must be " + want.name() + ", but this is " + from.name(); - if (from.isNumber() && want.isNumber()) - msg += " (use " + want.name() + "(...) to convert" + (from.isFloat() && want.isInt() ? ", it cuts off the fraction)" : ")"); - if (from.kind == FType::Null) msg = what + " can't be null (only pointers can)"; - fail(p.line, p.col, msg); - } - - // ---------- helpers ---------- - - bool terminated() { return b.GetInsertBlock()->getTerminator() != nullptr; } - - BasicBlock *newBlock(const char *name) { - return BasicBlock::Create(ctx, name, b.GetInsertBlock()->getParent()); - } - - // Variables live in stack slots created at the top of the function; LLVM turns them into registers. - AllocaInst *slot(const FType &t, const std::string &name) { - Function *f = b.GetInsertBlock()->getParent(); - IRBuilder<> entry(&f->getEntryBlock(), f->getEntryBlock().begin()); - return entry.CreateAlloca(ty(t), nullptr, name); - } - - Var *lookup(const std::string &name) { - for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { - auto found = it->find(name); - if (found != it->end()) return &found->second; - } - return nullptr; - } - - void addVar(Pos p, const std::string &name, const FType &t, Value *init, bool readonly = false) { - if (lookup(name)) fail(p.line, p.col, "a variable named '" + name + "' already exists here"); - if (fns.count(name)) fail(p.line, p.col, "'" + name + "' is already the name of a function"); - if (isBuiltin(name)) fail(p.line, p.col, "'" + name + "' is a built-in name, pick another one"); - AllocaInst *s = slot(t, name); - b.CreateStore(init, s); - scopes.back()[name] = {s, t, readonly}; - } - - Function *libc(const char *name, llvm::Type *ret, std::vector params, bool vararg = false) { - return cast(mod->getOrInsertFunction(name, FunctionType::get(ret, params, vararg)).getCallee()); - } - - // ---------- functions ---------- - - void declare(const FnDecl &fn) { - if (fns.count(fn.name)) fail(fn.pos.line, fn.pos.col, "function '" + fn.name + "' is defined twice"); - if (isBuiltin(fn.name)) fail(fn.pos.line, fn.pos.col, "'" + fn.name + "' is a built-in function, pick another name"); - - bool isMain = fn.name == "main"; - if (isMain && !fn.params.empty()) fail(fn.pos.line, fn.pos.col, "main takes no parameters"); - if (isMain && fn.ret.kind != FType::Void && fn.ret.kind != FType::I64 && fn.ret.kind != FType::I32) - fail(fn.pos.line, fn.pos.col, "main returns nothing (fn main()) or an exit code (fn main() -> int)"); - - std::vector params; - for (const Param &p : fn.params) params.push_back(ty(p.type)); - llvm::Type *ret = isMain ? b.getInt32Ty() : ty(fn.ret); - - // User functions get a prefix so they never clash with C library names. - Function *f = Function::Create(FunctionType::get(ret, params, false), - isMain ? Function::ExternalLinkage : Function::InternalLinkage, - isMain ? "main" : "finch." + fn.name, mod.get()); - fns[fn.name] = {f, &fn}; - } - - void define(const FnDecl &fn) { - Function *f = fns[fn.name].llvm; - curFn = &fn; - b.SetInsertPoint(BasicBlock::Create(ctx, "entry", f)); - - scopes.push_back({}); - unsigned i = 0; - for (Argument &arg : f->args()) { - const Param &p = fn.params[i++]; - arg.setName(p.name); - addVar(p.pos, p.name, p.type, &arg); - } - blockBody(*fn.body); - scopes.pop_back(); - - if (!terminated()) { - if (fn.name == "main") b.CreateRet(b.getInt32(0)); - else if (fn.ret.kind == FType::Void) b.CreateRetVoid(); - else fail(fn.pos.line, fn.pos.col, "function '" + fn.name + "' can reach its end without returning " + fn.ret.name()); - } - } - - // ---------- statements ---------- - - void block(const BlockStmt &blk) { - scopes.push_back({}); - blockBody(blk); - scopes.pop_back(); - } - - void blockBody(const BlockStmt &blk) { - for (const StmtPtr &s : blk.body) { - if (terminated()) fail(s->pos.line, s->pos.col, "this code can never run (it comes after return, break or continue)"); - stmt(*s); - } - } - - void stmt(const Stmt &s) { - switch (s.kind) { - case StmtKind::Block: block(static_cast(s)); break; - case StmtKind::VarDecl: varDecl(static_cast(s)); break; - case StmtKind::Assign: assign(static_cast(s)); break; - case StmtKind::Expr: expr(*static_cast(s).expr); break; - case StmtKind::If: ifStmt(static_cast(s)); break; - case StmtKind::While: whileStmt(static_cast(s)); break; - case StmtKind::For: forStmt(static_cast(s)); break; - case StmtKind::Return: returnStmt(static_cast(s)); break; - case StmtKind::Break: - case StmtKind::Continue: { - bool isBreak = s.kind == StmtKind::Break; - if (loops.empty()) fail(s.pos.line, s.pos.col, std::string(isBreak ? "break" : "continue") + " can only be used inside a loop"); - b.CreateBr(isBreak ? loops.back().breakTo : loops.back().continueTo); + // `return local` moves the variable out instead of copying it. + if (s.value->kind == ExprKind::Var) { + const std::string &name = static_cast(*s.value).name; + for (size_t idx = scopes.size(); idx-- > 0;) { + auto it = scopes[idx].vars.find(name); + if (it == scopes[idx].vars.end()) continue; + if (it->second.owned && it->second.type == curFn->ret) { + Value *v = b.CreateLoad(ty(it->second.type), it->second.slot); + it->second.moved = true; + emitCleanups(0); + scopes[idx].vars[name].moved = false; + b.CreateRet(v); + return; + } break; } - } } + Value_ v = coerce(exprWant(*s.value, curFn->ret), curFn->ret, s.value->pos, "the returned value"); + Value *out = own(v); + emitCleanups(0); + b.CreateRet(out); +} - void varDecl(const VarDeclStmt &s) { - if (!s.hasType) { - Value_ v = expr(*s.init); - if (v.type.kind == FType::Void) fail(s.init->pos.line, s.init->pos.col, "this gives back nothing, so it can't be stored"); - if (v.type.kind == FType::Null) fail(s.init->pos.line, s.init->pos.col, "null needs a type, like: ptr[int] " + s.name + " = null"); - addVar(s.pos, s.name, v.type, v.v); - return; - } - Value *init = s.init ? coerce(expr(*s.init), s.type, s.init->pos, "'" + s.name + "'") : zero(s.type); - addVar(s.pos, s.name, s.type, init); +// ---------- mutation analysis (does a function or loop change a variable?) ---------- + +std::string Codegen::rootVar(const Expr &e) { + switch (e.kind) { + case ExprKind::Var: return static_cast(e).name; + case ExprKind::Member: return rootVar(*static_cast(e).obj); + case ExprKind::Index: return rootVar(*static_cast(e).obj); + default: return ""; } +} - // Where does `x` or `p.value` live in memory? - Place place(const Expr &e) { - if (e.kind == ExprKind::Var) { - auto &v = static_cast(e); - Var *var = lookup(v.name); - if (!var) { - if (cimports.globals.count(v.name)) return {cGlobal(v.name), cimports.globals.at(v.name).type}; - fail(e.pos.line, e.pos.col, "there is no variable named '" + v.name + "'"); - } - return {var->slot, var->type}; - } - if (e.kind == ExprKind::Member) { - auto &m = static_cast(e); - Value_ obj = expr(*m.obj); - if (!obj.type.isPtr()) - fail(m.pos.line, m.pos.col, obj.type.name() + " has no '." + m.field + "'"); - if (m.field != "value") - fail(m.pos.line, m.pos.col, "a pointer only has '.value' (the thing it points to)"); - if (!obj.type.elem) - fail(m.pos.line, m.pos.col, "this ptr has no type, so Finch doesn't know what's inside (use ptr[int] etc.)"); - // reading or writing through null is undefined in C; in Finch it stops the program - if (!isa(obj.v) && !isa(obj.v)) - panicIf(b.CreateIsNull(obj.v), "used .value on a null pointer", m.pos); - return {obj.v, *obj.type.elem}; - } - fail(e.pos.line, e.pos.col, "this can't be assigned to"); +bool Codegen::mutatesExpr(const std::string &name, const Expr &e) { + static const std::set changing = {"push", "pop", "insert", "remove", "clear", "resize", "sort", "reverse"}; + switch (e.kind) { + case ExprKind::Unary: return mutatesExpr(name, *static_cast(e).operand); + case ExprKind::Binary: { + auto &x = static_cast(e); + return mutatesExpr(name, *x.lhs) || mutatesExpr(name, *x.rhs); } - - void assign(const AssignStmt &s) { - if (s.target->kind == ExprKind::Var) { - auto &name = static_cast(*s.target).name; - Var *var = lookup(name); - if (!var && !cimports.globals.count(name)) - fail(s.pos.line, s.pos.col, "there is no variable named '" + name + "' (create it with " + name + " := ...)"); - if (var && var->readonly) fail(s.pos.line, s.pos.col, "the loop variable '" + name + "' can't be changed"); - } - Place dst = place(*s.target); - std::string what = s.target->kind == ExprKind::Var ? "'" + static_cast(*s.target).name + "'" : "the value"; - - Value *v; - if (s.op == '=') { - v = coerce(expr(*s.value), dst.type, s.value->pos, what); - } else { - BinOp op = s.op == '+' ? BinOp::Add : s.op == '-' ? BinOp::Sub : s.op == '*' ? BinOp::Mul - : s.op == '/' ? BinOp::Div : BinOp::Mod; - Value_ cur{b.CreateLoad(ty(dst.type), dst.addr), dst.type}; - v = coerce(arith(op, cur, expr(*s.value), s.pos), dst.type, s.pos, what); - } - b.CreateStore(v, dst.addr); - } - - Value *condition(const Expr &e) { return coerce(expr(e), FType::Bool, e.pos, "a condition"); } - - void ifStmt(const IfStmt &s) { - Value *cond = condition(*s.cond); - BasicBlock *thenBB = newBlock("then"); - BasicBlock *elseBB = s.otherwise ? newBlock("else") : nullptr; - BasicBlock *end = newBlock("endif"); - b.CreateCondBr(cond, thenBB, elseBB ? elseBB : end); - - b.SetInsertPoint(thenBB); - block(*s.then); - BasicBlock *last = b.GetInsertBlock(); - if (!terminated()) b.CreateBr(end); - - if (elseBB) { - b.SetInsertPoint(elseBB); - stmt(*s.otherwise); - last = b.GetInsertBlock(); - if (!terminated()) b.CreateBr(end); - } - continueAt(end, last); - } - - // Continue emitting into `bb`; if nothing jumps there, drop it and stay "after a return". - void continueAt(BasicBlock *bb, BasicBlock *deadEnd) { - if (pred_empty(bb)) { - bb->eraseFromParent(); - b.SetInsertPoint(deadEnd); - } else { - b.SetInsertPoint(bb); - } - } - - void whileStmt(const WhileStmt &s) { - BasicBlock *condBB = newBlock("while.cond"); - BasicBlock *body = newBlock("while.body"); - BasicBlock *end = newBlock("while.end"); - b.CreateBr(condBB); - - b.SetInsertPoint(condBB); - Value *cond = condition(*s.cond); - if (auto *c = dyn_cast(cond); c && c->isOne()) b.CreateBr(body); // while true - else b.CreateCondBr(cond, body, end); - - b.SetInsertPoint(body); - loops.push_back({condBB, end}); - block(*s.body); - loops.pop_back(); - if (!terminated()) b.CreateBr(condBB); - continueAt(end, b.GetInsertBlock()); - } - - void forStmt(const ForStmt &s) { - Value *from = coerce(expr(*s.from), FType::I64, s.from->pos, "the start of a range"); - Value *to = coerce(expr(*s.to), FType::I64, s.to->pos, "the end of a range"); - - scopes.push_back({}); - addVar(s.pos, s.var, FType::I64, from, true); - AllocaInst *i = scopes.back()[s.var].slot; - - BasicBlock *condBB = newBlock("for.cond"); - BasicBlock *body = newBlock("for.body"); - BasicBlock *step = newBlock("for.step"); - BasicBlock *end = newBlock("for.end"); - b.CreateBr(condBB); - - b.SetInsertPoint(condBB); - b.CreateCondBr(b.CreateICmpSLT(b.CreateLoad(b.getInt64Ty(), i), to), body, end); - - b.SetInsertPoint(body); - loops.push_back({step, end}); - block(*s.body); - loops.pop_back(); - if (!terminated()) b.CreateBr(step); - - b.SetInsertPoint(step); - b.CreateStore(b.CreateAdd(b.CreateLoad(b.getInt64Ty(), i), b.getInt64(1)), i); - b.CreateBr(condBB); - - scopes.pop_back(); - b.SetInsertPoint(end); - } - - void returnStmt(const ReturnStmt &s) { - bool isMain = curFn->name == "main"; - if (curFn->ret.kind == FType::Void) { - if (s.value) fail(s.value->pos.line, s.value->pos.col, "'" + curFn->name + "' doesn't return a value (it has no '-> type')"); - if (isMain) b.CreateRet(b.getInt32(0)); - else b.CreateRetVoid(); - return; - } - if (!s.value) fail(s.pos.line, s.pos.col, "'" + curFn->name + "' must return " + curFn->ret.name()); - Value *v = coerce(expr(*s.value), curFn->ret, s.value->pos, "the returned value"); - b.CreateRet(isMain ? b.CreateIntCast(v, b.getInt32Ty(), true) : v); - } - - // ---------- expressions ---------- - - Value_ expr(const Expr &e) { - switch (e.kind) { - case ExprKind::Int: return {b.getInt64(static_cast(e).value), FType::I64, true}; - case ExprKind::Float: return {ConstantFP::get(b.getDoubleTy(), static_cast(e).value), FType::F64, true}; - case ExprKind::Bool: return {b.getInt1(static_cast(e).value), FType::Bool}; - case ExprKind::Char: return {b.getInt8(static_cast(e).value), FType::Char}; - case ExprKind::Str: return {b.CreateGlobalString(static_cast(e).value, "str"), FType::Str}; - case ExprKind::Null: return {ConstantPointerNull::get(b.getPtrTy()), FType::Null}; - case ExprKind::Var: { - auto &v = static_cast(e); - if (!lookup(v.name)) { - auto c = cimports.consts.find(v.name); - if (c != cimports.consts.end()) { - if (c->second.type.isFloat()) return {ConstantFP::get(b.getDoubleTy(), c->second.f), FType::F64, true}; - return {b.getInt64(c->second.i), FType::I64, true}; - } - } - Place pl = place(e); - return {b.CreateLoad(ty(pl.type), pl.addr, v.name), pl.type}; - } - case ExprKind::Member: { - Place pl = place(e); - return {b.CreateLoad(ty(pl.type), pl.addr), pl.type}; - } - case ExprKind::Unary: { - auto &u = static_cast(e); - Value_ v = expr(*u.operand); - if (u.op == '!') return {b.CreateNot(coerce(v, FType::Bool, u.operand->pos, "the value after '!'")), FType::Bool}; - if (u.op == '~') { - if (!v.type.isInt()) fail(e.pos.line, e.pos.col, "'~' flips the bits of a whole number, not of " + v.type.name()); - return {b.CreateNot(v.v), v.type}; - } - if (v.type.isSigned()) return {b.CreateNeg(v.v), v.type, v.literal}; - if (v.type.isFloat()) return {b.CreateFNeg(v.v), v.type, v.literal}; - if (v.type.isUnsigned()) fail(e.pos.line, e.pos.col, v.type.name() + " can't be negative (it's unsigned)"); - fail(e.pos.line, e.pos.col, "can't put '-' in front of " + v.type.name()); - } - case ExprKind::Binary: return binary(static_cast(e)); - case ExprKind::Call: return call(static_cast(e)); - } - return {nullptr, FType::Void}; - } - - static const char *opName(BinOp op) { - switch (op) { - case BinOp::Add: return "+"; - case BinOp::Sub: return "-"; - case BinOp::Mul: return "*"; - case BinOp::Div: return "/"; - case BinOp::Mod: return "%"; - case BinOp::BitAnd: return "&"; - case BinOp::BitOr: return "|"; - case BinOp::BitXor: return "^"; - case BinOp::Shl: return "<<"; - case BinOp::Shr: return ">>"; - case BinOp::Eq: return "=="; - case BinOp::NotEq: return "!="; - case BinOp::Less: return "<"; - case BinOp::LessEq: return "<="; - case BinOp::Greater: return ">"; - case BinOp::GreaterEq: return ">="; - case BinOp::And: return "&&"; - case BinOp::Or: return "||"; - } - return "?"; - } - - [[noreturn]] void badOperands(BinOp op, const Value_ &l, const Value_ &r, Pos p) { - std::string msg = std::string("can't use '") + opName(op) + "' on " + l.type.name() + - (l.type == r.type ? "" : " and " + r.type.name()); - if (l.type.isInt() && r.type.isInt() && l.type != r.type) msg += " (convert one side, like " + l.type.name() + "(...))"; - if (l.type.kind == FType::Bool && (op == BinOp::BitAnd || op == BinOp::BitOr)) - msg += std::string(" (for true/false use '") + (op == BinOp::BitAnd ? "&&" : "||") + "')"; - else if ((l.type.isFloat() || r.type.isFloat()) && op >= BinOp::BitAnd && op <= BinOp::Shr) - msg += " (bit operators work on whole numbers only)"; - fail(p.line, p.col, msg); - } - - // Bring both sides to one type: a number literal adapts to the other side, - // otherwise the smaller type grows into the bigger one. - bool unify(Value_ &l, Value_ &r) { - if (l.type == r.type) return true; - if (l.type.isInt() && r.type.isInt()) { - auto *lc = l.literal ? dyn_cast(l.v) : nullptr; - auto *rc = r.literal ? dyn_cast(r.v) : nullptr; - if (lc && !rc && fits(lc, l.type, r.type)) { l = {intCast(l.v, l.type, r.type), r.type, true}; return true; } - if (rc && !lc && fits(rc, r.type, l.type)) { r = {intCast(r.v, r.type, l.type), l.type, true}; return true; } - if (widens(l.type, r.type)) { l = {intCast(l.v, l.type, r.type), r.type}; return true; } - if (widens(r.type, l.type)) { r = {intCast(r.v, r.type, l.type), l.type}; return true; } - return false; - } - if (l.type.isInt() && r.type.isFloat()) { l = {intToFloat(l.v, l.type, r.type), r.type, l.literal}; return true; } - if (l.type.isFloat() && r.type.isInt()) { r = {intToFloat(r.v, r.type, l.type), l.type, r.literal}; return true; } - if (l.type.isFloat() && r.type.isFloat()) { - if (l.type.kind == FType::F32) l = {b.CreateFPExt(l.v, b.getDoubleTy()), FType::F64}; - if (r.type.kind == FType::F32) r = {b.CreateFPExt(r.v, b.getDoubleTy()), FType::F64}; - return true; - } + case ExprKind::Call: { + auto &c = static_cast(e); + if (c.callee == "addr" && c.args.size() == 1 && rootVar(*c.args[0]) == name) return true; + for (auto &a : c.args) + if (mutatesExpr(name, *a)) return true; return false; } - - Value_ arith(BinOp op, Value_ l, Value_ r, Pos p) { - Value_ v = arithOp(op, l, r, p); - v.literal = l.literal && r.literal && isa(v.v); - return v; + case ExprKind::Member: return mutatesExpr(name, *static_cast(e).obj); + case ExprKind::Index: { + auto &x = static_cast(e); + return mutatesExpr(name, *x.obj) || mutatesExpr(name, *x.index); } + case ExprKind::ArrayLit: + for (auto &a : static_cast(e).elems) + if (mutatesExpr(name, *a)) return true; + return false; + case ExprKind::Method: { + auto &m = static_cast(e); + if (changing.count(m.name) && rootVar(*m.obj) == name) return true; + if (mutatesExpr(name, *m.obj)) return true; + for (auto &a : m.args) + if (mutatesExpr(name, *a)) return true; + return false; + } + default: return false; + } +} - Value_ arithOp(BinOp op, Value_ l, Value_ r, Pos p) { - if (!unify(l, r)) badOperands(op, l, r, p); - if (l.type.isInt()) { - bool s = l.type.isSigned(); - switch (op) { - case BinOp::Add: return {b.CreateAdd(l.v, r.v), l.type}; - case BinOp::Sub: return {b.CreateSub(l.v, r.v), l.type}; - case BinOp::Mul: return {b.CreateMul(l.v, r.v), l.type}; - case BinOp::Div: - checkDivisor(l, r, p); - return {s ? b.CreateSDiv(l.v, r.v) : b.CreateUDiv(l.v, r.v), l.type}; - case BinOp::Mod: - checkDivisor(l, r, p); - return {s ? b.CreateSRem(l.v, r.v) : b.CreateURem(l.v, r.v), l.type}; - case BinOp::BitAnd: return {b.CreateAnd(l.v, r.v), l.type}; - case BinOp::BitOr: return {b.CreateOr(l.v, r.v), l.type}; - case BinOp::BitXor: return {b.CreateXor(l.v, r.v), l.type}; - case BinOp::Shl: return {b.CreateShl(l.v, r.v), l.type}; - case BinOp::Shr: return {s ? b.CreateAShr(l.v, r.v) : b.CreateLShr(l.v, r.v), l.type}; - default: break; +bool Codegen::mutates(const std::string &name, const Stmt &s) { + auto e = [&](const ExprPtr &x) { return x && mutatesExpr(name, *x); }; + switch (s.kind) { + case StmtKind::Block: + for (auto &st : static_cast(s).body) + if (mutates(name, *st)) return true; + return false; + case StmtKind::VarDecl: return e(static_cast(s).init); + case StmtKind::Assign: { + auto &a = static_cast(s); + return rootVar(*a.target) == name || e(a.target) || e(a.value); + } + case StmtKind::Expr: return e(static_cast(s).expr); + case StmtKind::If: { + auto &x = static_cast(s); + return e(x.cond) || mutates(name, *x.then) || (x.otherwise && mutates(name, *x.otherwise)); + } + case StmtKind::While: { + auto &x = static_cast(s); + return e(x.cond) || mutates(name, *x.body); + } + case StmtKind::For: { + auto &x = static_cast(s); + return e(x.from) || e(x.to) || mutates(name, *x.body); + } + case StmtKind::ForEach: { + auto &x = static_cast(s); + return e(x.list) || mutates(name, *x.body); + } + case StmtKind::Return: return e(static_cast(s).value); + case StmtKind::Defer: return mutates(name, *static_cast(s).body); + default: return false; + } +} + +// ---------- expressions ---------- + +Value_ Codegen::exprWant(const Expr &e, const FType &want) { + if (e.kind == ExprKind::ArrayLit) return arrayLit(static_cast(e), &want); + return expr(e); +} + +Value_ Codegen::expr(const Expr &e) { + switch (e.kind) { + case ExprKind::Int: return {b.getInt64(static_cast(e).value), FType::I64, true}; + case ExprKind::Float: return {ConstantFP::get(b.getDoubleTy(), static_cast(e).value), FType::F64, true}; + case ExprKind::Bool: return {b.getInt1(static_cast(e).value), FType::Bool}; + case ExprKind::Char: return {b.getInt8(static_cast(e).value), FType::Char}; + case ExprKind::Str: return {strConst(static_cast(e).value), FType::Str, false, true}; + case ExprKind::Null: return {ConstantPointerNull::get(b.getPtrTy()), FType::Null}; + case ExprKind::Var: { + auto &v = static_cast(e); + if (!lookup(v.name)) { + auto c = cimports.consts.find(v.name); + if (c != cimports.consts.end()) { + if (c->second.type.isFloat()) return {ConstantFP::get(b.getDoubleTy(), c->second.f), FType::F64, true}; + return {b.getInt64(c->second.i), FType::I64, true}; } - } - if (l.type.isFloat()) { - switch (op) { - case BinOp::Add: return {b.CreateFAdd(l.v, r.v), l.type}; - case BinOp::Sub: return {b.CreateFSub(l.v, r.v), l.type}; - case BinOp::Mul: return {b.CreateFMul(l.v, r.v), l.type}; - case BinOp::Div: return {b.CreateFDiv(l.v, r.v), l.type}; - case BinOp::Mod: return {b.CreateFRem(l.v, r.v), l.type}; - default: break; - } - } - badOperands(op, l, r, p); - } - - // ---------- runtime errors ---------- - - // finch.panic(msg, line): print "file:line: runtime error: msg" and stop the program. - Function *panicFn() { - if (Function *f = mod->getFunction("finch.panic")) return f; - Function *f = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy(), b.getInt64Ty()}, false), - Function::InternalLinkage, "finch.panic", mod.get()); - f->addFnAttr(Attribute::NoReturn); - f->addFnAttr(Attribute::Cold); - f->addFnAttr(Attribute::NoInline); - IRBuilder<> pb(BasicBlock::Create(ctx, "entry", f)); - Function *dprintf = libc("dprintf", b.getInt32Ty(), {b.getInt32Ty(), b.getPtrTy()}, true); - pb.CreateCall(dprintf, {pb.getInt32(2), pb.CreateGlobalString("%s:%lld: runtime error: %s\n", "panic.fmt"), - pb.CreateGlobalString(g_file, "panic.file"), f->getArg(1), f->getArg(0)}); - pb.CreateCall(libc("exit", b.getVoidTy(), {b.getInt32Ty()}), {pb.getInt32(1)}); - pb.CreateUnreachable(); - return f; - } - - // if (cond) panic(msg) - void panicIf(Value *cond, const char *msg, Pos p) { - BasicBlock *bad = newBlock("panic"); - BasicBlock *ok = newBlock("ok"); - b.CreateCondBr(cond, bad, ok); - b.SetInsertPoint(bad); - b.CreateCall(panicFn(), {b.CreateGlobalString(msg, "panic.msg"), b.getInt64(p.line)}); - b.CreateUnreachable(); - b.SetInsertPoint(ok); - } - - // Dividing by zero is undefined in C; in Finch it is a clear error. - void checkDivisor(const Value_ &l, const Value_ &r, Pos p) { - auto *c = dyn_cast(r.v); - if (c && c->isZero()) fail(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_ compare(BinOp op, Value_ l, Value_ r, Pos p) { - bool equality = op == BinOp::Eq || op == BinOp::NotEq; - auto pointerLike = [](const FType &t) { return t.isPtr() || t.kind == FType::Str || t.kind == FType::Null; }; - - // Pointers (and `s == null`) compare addresses. - if (pointerLike(l.type) && pointerLike(r.type) && - (l.type.isPtr() || r.type.isPtr() || l.type.kind == FType::Null || r.type.kind == FType::Null)) { - if (!equality) badOperands(op, l, r, p); - bool bothPtr = (l.type.isPtr() || l.type.kind == FType::Null) && (r.type.isPtr() || r.type.kind == FType::Null); - bool strNull = (l.type.kind == FType::Str && r.type.kind == FType::Null) || (r.type.kind == FType::Str && l.type.kind == FType::Null); - if (!bothPtr && !strNull) badOperands(op, l, r, p); - return {op == BinOp::Eq ? b.CreateICmpEQ(l.v, r.v) : b.CreateICmpNE(l.v, r.v), FType::Bool}; - } - - if (!unify(l, r)) badOperands(op, l, r, p); - - if (l.type.kind == FType::Str) { - if (!equality) badOperands(op, l, r, p); - Function *strcmp = libc("strcmp", b.getInt32Ty(), {b.getPtrTy(), b.getPtrTy()}); - Value *c = b.CreateCall(strcmp, {l.v, r.v}); - return {op == BinOp::Eq ? b.CreateICmpEQ(c, b.getInt32(0)) : b.CreateICmpNE(c, b.getInt32(0)), FType::Bool}; - } - if (l.type.isFloat()) { - CmpInst::Predicate pr; - switch (op) { - case BinOp::Eq: pr = CmpInst::FCMP_OEQ; break; - case BinOp::NotEq: pr = CmpInst::FCMP_UNE; break; - case BinOp::Less: pr = CmpInst::FCMP_OLT; break; - case BinOp::LessEq: pr = CmpInst::FCMP_OLE; break; - case BinOp::Greater: pr = CmpInst::FCMP_OGT; break; - default: pr = CmpInst::FCMP_OGE; break; - } - return {b.CreateFCmp(pr, l.v, r.v), FType::Bool}; - } - if (l.type.kind == FType::Bool && !equality) badOperands(op, l, r, p); - bool s = l.type.isSigned(); - CmpInst::Predicate pr; - switch (op) { - case BinOp::Eq: pr = CmpInst::ICMP_EQ; break; - case BinOp::NotEq: pr = CmpInst::ICMP_NE; break; - case BinOp::Less: pr = s ? CmpInst::ICMP_SLT : CmpInst::ICMP_ULT; break; - case BinOp::LessEq: pr = s ? CmpInst::ICMP_SLE : CmpInst::ICMP_ULE; break; - case BinOp::Greater: pr = s ? CmpInst::ICMP_SGT : CmpInst::ICMP_UGT; break; - default: pr = s ? CmpInst::ICMP_SGE : CmpInst::ICMP_UGE; break; - } - return {b.CreateICmp(pr, l.v, r.v), FType::Bool}; - } - - // && and || only evaluate the right side when needed. - Value_ logic(const BinaryExpr &e) { - bool isAnd = e.op == BinOp::And; - Value *l = condition(*e.lhs); - BasicBlock *from = b.GetInsertBlock(); - BasicBlock *rhsBB = newBlock(isAnd ? "and.rhs" : "or.rhs"); - BasicBlock *end = newBlock(isAnd ? "and.end" : "or.end"); - if (isAnd) b.CreateCondBr(l, rhsBB, end); - else b.CreateCondBr(l, end, rhsBB); - - b.SetInsertPoint(rhsBB); - Value *r = condition(*e.rhs); - BasicBlock *rhsEnd = b.GetInsertBlock(); - b.CreateBr(end); - - b.SetInsertPoint(end); - PHINode *phi = b.CreatePHI(b.getInt1Ty(), 2); - phi->addIncoming(b.getInt1(!isAnd), from); - phi->addIncoming(r, rhsEnd); - return {phi, FType::Bool}; - } - - Value_ binary(const BinaryExpr &e) { - switch (e.op) { - case BinOp::And: - case BinOp::Or: return logic(e); - case BinOp::Eq: - case BinOp::NotEq: - case BinOp::Less: - case BinOp::LessEq: - case BinOp::Greater: - case BinOp::GreaterEq: { - Value_ l = expr(*e.lhs); - return compare(e.op, l, expr(*e.rhs), e.pos); - } - default: { - Value_ l = expr(*e.lhs); - return arith(e.op, l, expr(*e.rhs), e.pos); - } - } - } - - // ---------- calls & built-ins ---------- - - static bool isBuiltin(const std::string &n) { - FType t; - return n == "print" || n == "addr" || typeFromName(n, t); - } - - Value_ call(const CallExpr &c) { - if (c.callee == "print") return print(c); - if (c.callee == "addr") return addrOf(c); - FType target; - if (typeFromName(c.callee, target)) return convert(c, target); - - auto it = fns.find(c.callee); - if (it != fns.end()) return callFinch(c, it->second); - - auto cf = cimports.fns.find(c.callee); - if (cf != cimports.fns.end()) return callC(c, cf->second); - - if (lookup(c.callee)) fail(c.pos.line, c.pos.col, "'" + c.callee + "' is a variable, not a function"); - fail(c.pos.line, c.pos.col, "there is no function named '" + c.callee + "'" + headerHint(c.callee)); - } - - // Help people who forgot an import. - static std::string headerHint(const std::string &name) { - static const std::unordered_map 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"}, {"free", "stdlib.h"}, {"rand", "stdlib.h"}, {"exit", "stdlib.h"}, - {"abs", "stdlib.h"}, {"atoi", "stdlib.h"}, {"strlen", "string.h"}, {"strcpy", "string.h"}, - {"memcpy", "string.h"}, {"time", "time.h"}, {"sleep", "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 checkArgCount(const CallExpr &c, size_t want, bool variadic) { - if (c.args.size() == want || (variadic && c.args.size() > want)) return; - fail(c.pos.line, c.pos.col, "'" + c.callee + "' takes " + (variadic ? "at least " : "") + std::to_string(want) + - " argument(s), but got " + std::to_string(c.args.size())); - } - - Value_ callFinch(const CallExpr &c, const Fn &fn) { - const FnDecl &d = *fn.decl; - if (d.name == "main") fail(c.pos.line, c.pos.col, "main can't be called, it is where the program starts"); - checkArgCount(c, d.params.size(), false); - std::vector args; - for (size_t i = 0; i < c.args.size(); i++) - args.push_back(coerce(expr(*c.args[i]), d.params[i].type, c.args[i]->pos, "argument '" + d.params[i].name + "'")); - return {b.CreateCall(fn.llvm, args), d.ret}; - } - - // C wants small integers widened by the caller: signed ones sign-extended, the rest zero-extended. - 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; - } - - Value_ callC(const CallExpr &c, const CFunc &f) { - if (!f.unsupported.empty()) - fail(c.pos.line, c.pos.col, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported); - checkArgCount(c, f.params.size(), f.variadic); - - std::vector ptypes; - for (const FType &t : f.params) ptypes.push_back(ty(t)); - Function *fn = libc(f.name.c_str(), ty(f.ret), ptypes, f.variadic); - - std::vector args; - for (size_t i = 0; i < c.args.size(); i++) { - Value_ v = expr(*c.args[i]); - if (i < f.params.size()) { - args.push_back(coerce(v, f.params[i], c.args[i]->pos, "argument " + std::to_string(i + 1) + " of " + f.name)); - continue; - } - // extra arguments of printf-like functions follow C's promotion rules - const FType &t = v.type; - if (t.kind == FType::Void) fail(c.args[i]->pos.line, c.args[i]->pos.col, "this gives back nothing"); - if (t.kind == FType::F32) v.v = b.CreateFPExt(v.v, b.getDoubleTy()); - else if (t.kind == FType::Bool || ((t.isInt() || t.kind == FType::Char) && t.bits() < 32)) - v.v = b.CreateIntCast(v.v, b.getInt32Ty(), t.isSigned() || t.kind == FType::Char); - args.push_back(v.v); - } - - CallInst *call = b.CreateCall(fn, args); - for (size_t i = 0; i < f.params.size(); i++) { - Attribute::AttrKind a = extAttr(f.params[i]); - if (a != Attribute::None) { - fn->addParamAttr(i, a); - call->addParamAttr(i, a); - } - } - if (Attribute::AttrKind a = extAttr(f.ret); a != Attribute::None) { - fn->addRetAttr(a); - call->addRetAttr(a); - } - return {call, f.ret}; - } - - Value *cGlobal(const std::string &name) { - const CGlobal &g = cimports.globals.at(name); - if (GlobalVariable *gv = mod->getNamedGlobal(name)) return gv; - return new GlobalVariable(*mod, ty(g.type), false, GlobalValue::ExternalLinkage, nullptr, name); - } - - // addr(x) gives a pointer to x - Value_ addrOf(const CallExpr &c) { - if (c.args.size() != 1) fail(c.pos.line, c.pos.col, "addr(...) takes exactly one variable"); - const Expr &e = *c.args[0]; - if (e.kind != ExprKind::Var && e.kind != ExprKind::Member) - fail(e.pos.line, e.pos.col, "addr(...) needs a variable, like addr(x)"); - if (e.kind == ExprKind::Var) { - Var *v = lookup(static_cast(e).name); - if (v && v->readonly) fail(e.pos.line, e.pos.col, "can't take the address of a loop variable"); + if (modules[curModule].imports.count(v.name)) + failAt(e.pos.file, e.pos.line, e.pos.col, "'" + v.name + "' is a module; use something from it, like " + v.name + ".name(...)"); } Place pl = place(e); - return {pl.addr, FType::ptrTo(pl.type)}; + return {b.CreateLoad(ty(pl.type), pl.addr, v.name), pl.type}; } - - // print(a, b, c) prints the values separated by spaces, then a newline. - Value_ print(const CallExpr &c) { - std::string fmt; - std::vector args; - for (size_t i = 0; i < c.args.size(); i++) { - if (i) fmt += ' '; - Value_ v = expr(*c.args[i]); - const FType &t = v.type; - if (t.isSigned()) { fmt += "%lld"; args.push_back(b.CreateSExt(v.v, b.getInt64Ty())); } - else if (t.isUnsigned()) { fmt += "%llu"; args.push_back(b.CreateZExt(v.v, b.getInt64Ty())); } - else if (t.isFloat()) { fmt += "%g"; args.push_back(b.CreateFPExt(v.v, b.getDoubleTy())); } - else if (t.kind == FType::Char) { fmt += "%c"; args.push_back(b.CreateZExt(v.v, b.getInt32Ty())); } - else if (t.kind == FType::Str) { fmt += "%s"; args.push_back(v.v); } - else if (t.isPtr()) { fmt += "%p"; args.push_back(v.v); } - else if (t.kind == FType::Null) fmt += "null"; - else if (t.kind == FType::Bool) { - fmt += "%s"; - args.push_back(b.CreateSelect(v.v, b.CreateGlobalString("true", "true"), b.CreateGlobalString("false", "false"))); - } else fail(c.args[i]->pos.line, c.args[i]->pos.col, "this gives back nothing, so there is nothing to print"); - } - fmt += '\n'; - args.insert(args.begin(), b.CreateGlobalString(fmt, "fmt")); - b.CreateCall(libc("printf", b.getInt32Ty(), {b.getPtrTy()}, true), args); - return {nullptr, FType::Void}; + case ExprKind::Member: + case ExprKind::Index: { + LRef r = ref(e); + if (!r.isPlace) return r.val; + return {b.CreateLoad(ty(r.pl.type), r.pl.addr), r.pl.type}; } - - // Explicit conversions: int(x), u8(x), float(x), char(x), bool(x), ptr(x) ... - Value_ convert(const CallExpr &c, const FType &to) { - if (c.args.size() != 1) fail(c.pos.line, c.pos.col, c.callee + "(...) takes exactly one value"); - Value_ v = expr(*c.args[0]); - const FType &from = v.type; - Pos p = c.args[0]->pos; - if (from == to) return v; - - 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::Str || from.kind == FType::Null)) return {v.v, to}; - if (to.kind == FType::Str && from.isPtr()) return {v.v, to}; - fail(p.line, p.col, "can't turn " + from.name() + " into " + to.name()); + case ExprKind::Unary: return unary(static_cast(e)); + case ExprKind::Binary: return binary(static_cast(e)); + case ExprKind::Call: return call(static_cast(e)); + case ExprKind::Method: return method(static_cast(e)); + case ExprKind::ArrayLit: return arrayLit(static_cast(e), nullptr); } -}; - -} // namespace - -std::unique_ptr generate(const Program &prog, const CImports &c, LLVMContext &ctx, TargetMachine &tm) { - return Codegen(ctx, c, tm).run(prog); + return {nullptr, FType::Void}; +} + +Value_ Codegen::unary(const UnaryExpr &u) { + Pos p = u.pos; + Value_ v = expr(*u.operand); + if (u.op == '!') return {b.CreateNot(coerce(v, FType::Bool, u.operand->pos, "the value after '!'").v), FType::Bool}; + if (u.op == '~') { + if (!v.type.isInt()) failAt(p.file, p.line, p.col, "'~' flips the bits of a whole number, not of " + v.type.show()); + return {b.CreateNot(v.v), v.type}; + } + if (v.type.isSigned()) return {b.CreateNeg(v.v), v.type, v.literal}; + if (v.type.isFloat()) return {b.CreateFNeg(v.v), v.type, v.literal}; + if (v.type.isUnsigned()) failAt(p.file, p.line, p.col, v.type.show() + " can't be negative (it's unsigned)"); + failAt(p.file, p.line, p.col, "can't put '-' in front of " + v.type.show()); +} + +// An address for `e` if it has one (a variable, field, element, p.value); otherwise its value. +LRef Codegen::ref(const Expr &e, bool forWrite) { + switch (e.kind) { + case ExprKind::Var: { + auto &v = static_cast(e); + if (Var *var = lookup(v.name)) return {true, {var->slot, var->type}, {}}; + if (cimports.globals.count(v.name)) return {true, {cGlobal(v.name), resolve(cimports.globals.at(v.name).type, e.pos)}, {}}; + return {false, {}, expr(e)}; + } + case ExprKind::Member: return member(static_cast(e), forWrite); + case ExprKind::Index: return index(static_cast(e), forWrite); + default: return {false, {}, expr(e)}; + } +} + +Place Codegen::place(const Expr &e, bool forWrite) { + if (e.kind == ExprKind::Var) { + auto &v = static_cast(e); + if (!lookup(v.name) && !cimports.globals.count(v.name)) + failAt(e.pos.file, e.pos.line, e.pos.col, "there is no variable named '" + v.name + "'"); + } + LRef r = ref(e, forWrite); + if (!r.isPlace) failAt(e.pos.file, e.pos.line, e.pos.col, "this is a temporary value, it can't be changed or pointed to"); + return r.pl; +} + +LRef Codegen::member(const MemberExpr &m, bool forWrite) { + Pos p = m.pos; + if (m.obj->kind == ExprKind::Var) { + const std::string &mn = static_cast(*m.obj).name; + if (!lookup(mn) && modules[curModule].imports.count(mn)) + failAt(p.file, p.line, p.col, "modules only have functions and structs; call it: " + mn + "." + m.field + "(...)"); + } + LRef obj = ref(*m.obj, forWrite); + FType t = obj.isPlace ? obj.pl.type : obj.val.type; + + // pointers: p.value is the target, p.field reaches through to a struct's field + if (t.isPtr()) { + Value *pv = obj.isPlace ? b.CreateLoad(b.getPtrTy(), obj.pl.addr) : obj.val.v; + if (!t.elem) failAt(p.file, p.line, p.col, "this ptr has no type, so Finch doesn't know what's inside (use ptr[int] etc.)"); + if (!isa(pv) && !isa(pv)) + panicIf(b.CreateIsNull(pv), "used ." + m.field + " on a null pointer", p); + // p.value is the target -- unless the target is a struct with its own field called `value` + bool structHasValue = false; + if (t.elem->kind == FType::Struct) + for (auto &f : t.elem->info->fields) structHasValue |= f.name == "value" && !f.hidden; + if (m.field == "value" && !structHasValue) return {true, {pv, *t.elem}, {}}; + if (t.elem->kind != FType::Struct) + failAt(p.file, p.line, p.col, "a " + t.show() + " only has '.value' (the thing it points to)"); + obj = {true, {pv, *t.elem}, {}}; + t = *t.elem; + } + + if (t.kind == FType::Array || t.kind == FType::Str || t.kind == FType::Fixed) { + if (m.field != "len" && m.field != "ptr") + failAt(p.file, p.line, p.col, (t.kind == FType::Str ? "a str" : "an array") + std::string(" has .len and .ptr, not '.") + m.field + "'"); + if (forWrite) failAt(p.file, p.line, p.col, "." + m.field + " can't be changed directly" + (t.kind == FType::Array ? " (use resize, push or pop)" : "")); + if (t.kind == FType::Fixed) { + if (m.field == "len") return {false, {}, {b.getInt64(t.count), FType::I64}}; + if (!obj.isPlace) failAt(p.file, p.line, p.col, ".ptr of a temporary value would point to nothing"); + return {false, {}, {obj.pl.addr, FType::ptrTo(*t.elem)}}; + } + Value *v = obj.isPlace ? b.CreateLoad(ty(t), obj.pl.addr) : obj.val.v; + if (m.field == "len") { + Value *len = b.CreateExtractValue(v, 1); + if (!obj.isPlace) release(obj.val); + return {false, {}, {len, FType::I64}}; + } + if (!obj.isPlace && obj.val.fresh) failAt(p.file, p.line, p.col, ".ptr of a temporary value would point to freed memory"); + FType pt = t.kind == FType::Str ? FType::ptrTo(FType::Char) : FType::ptrTo(*t.elem); + return {false, {}, {t.kind == FType::Str ? cstr(v) : b.CreateExtractValue(v, 0), pt}}; + } + + if (t.kind != FType::Struct) failAt(p.file, p.line, p.col, t.show() + " has no fields (no '." + m.field + "')"); + StructInfo *s = t.info; + int idx = -1; + for (size_t k = 0; k < s->fields.size(); k++) + if (s->fields[k].name == m.field && !s->fields[k].hidden) idx = (int)k; + if (idx < 0) { + std::string names; + for (auto &f : s->fields) + if (!f.hidden) names += (names.empty() ? "" : ", ") + f.name; + failAt(p.file, p.line, p.col, "the struct " + t.show() + " has no field '" + m.field + "' (it has: " + names + ")"); + } + const StructInfo::F &f = s->fields[idx]; + unsigned llvmIdx = f.llvmIndex; + if (obj.isPlace) return {true, {b.CreateStructGEP(s->llvm, obj.pl.addr, llvmIdx), f.type}, {}}; + // a field of a temporary struct: take it (copying if it owns memory), then drop the rest + Value *fv = b.CreateExtractValue(obj.val.v, llvmIdx); + Value_ out{fv, f.type}; + if (owning(f.type) && obj.val.fresh) out = {copyValue(fv, f.type), f.type, false, true}; + release(obj.val); + return {false, {}, out}; +} + +LRef Codegen::index(const IndexExpr &e, bool forWrite) { + Pos p = e.pos; + LRef obj = ref(*e.obj, forWrite); + FType t = obj.isPlace ? obj.pl.type : obj.val.type; + Value *i = coerce(expr(*e.index), FType::I64, e.index->pos, "an index").v; + + if (t.isPtr()) { // C-style: p[i], no bounds known + if (!t.elem) failAt(p.file, p.line, p.col, "this ptr has no type, so Finch doesn't know what's inside (use ptr[int] etc.)"); + Value *pv = obj.isPlace ? b.CreateLoad(b.getPtrTy(), obj.pl.addr) : obj.val.v; + panicIf(b.CreateIsNull(pv), "used [] on a null pointer", p); + return {true, {b.CreateInBoundsGEP(ty(*t.elem), pv, i), *t.elem}, {}}; + } + if (t.kind == FType::Fixed) { + boundsCheck(i, b.getInt64(t.count), p); + if (obj.isPlace) return {true, {b.CreateInBoundsGEP(ty(t), obj.pl.addr, {b.getInt64(0), i}), *t.elem}, {}}; + Value *a = tmp(obj.val.v); + return {false, {}, {b.CreateLoad(ty(*t.elem), b.CreateInBoundsGEP(ty(t), a, {b.getInt64(0), i})), *t.elem}}; + } + if (t.kind != FType::Array && t.kind != FType::Str) + failAt(p.file, p.line, p.col, "[ ] works on arrays, str and pointers, not on " + t.show()); + FType et = t.kind == FType::Str ? FType(FType::Char) : *t.elem; + if (t.kind == FType::Str && forWrite) { + if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary str, it can't be changed"); + b.CreateCall(rt("finch_str_own"), {obj.pl.addr}); // literals live in read-only memory: copy first + } + Value *v = obj.isPlace ? b.CreateLoad(ty(t), obj.pl.addr) : obj.val.v; + Value *len = b.CreateExtractValue(v, 1); + boundsCheck(i, len, p); + Value *addr = b.CreateInBoundsGEP(ty(et), b.CreateExtractValue(v, 0), i); + if (obj.isPlace) return {true, {addr, et}, {}}; + Value *ev = b.CreateLoad(ty(et), addr); + Value_ out{ev, et}; + if (owning(et) && obj.val.fresh) out = {copyValue(ev, et), et, false, true}; + release(obj.val); + return {false, {}, out}; +} + +Value_ Codegen::arrayLit(const ArrayLitExpr &a, const FType *want) { + Pos p = a.pos; + FType et; + if (want && want->kind == FType::Array) { + et = *want->elem; + } else if (a.elems.empty()) { + failAt(p.file, p.line, p.col, "an empty [] needs a type, like: []int nums = []"); + } else { + // numbers written in the code: if any has a dot, they are all floats + bool allNum = true, anyFloat = false; + for (auto &x : a.elems) { + const Expr *y = x.get(); + if (y->kind == ExprKind::Unary && static_cast(y)->op == '-') y = static_cast(y)->operand.get(); + if (y->kind == ExprKind::Float) anyFloat = true; + else if (y->kind != ExprKind::Int) allNum = false; + } + et = FType::Void; + if (allNum && anyFloat) et = FType::F64; + } + std::vector vals; + for (size_t k = 0; k < a.elems.size(); k++) { + Value_ v = et.kind == FType::Void ? exprWant(*a.elems[k], et) : exprWant(*a.elems[k], et); + if (et.kind == FType::Void) { + if (v.type.kind == FType::Void || v.type.kind == FType::Null) + failAt(a.elems[k]->pos.file, a.elems[k]->pos.line, a.elems[k]->pos.col, "an array element needs a value with a type"); + et = v.type; + } + v = coerce(v, et, a.elems[k]->pos, "every element of this array"); + vals.push_back(own(v)); + } + FType at = FType::arrayOf(et); + Value *n = b.getInt64(vals.size()); + Value *data = vals.empty() ? (Value *)ConstantPointerNull::get(b.getPtrTy()) + : b.CreateCall(rt("finch_alloc"), {b.CreateMul(n, elemSize(et))}); + for (size_t k = 0; k < vals.size(); k++) b.CreateStore(vals[k], b.CreateInBoundsGEP(ty(et), data, b.getInt64(k))); + Value *arr = UndefValue::get(ty(at)); + arr = b.CreateInsertValue(arr, data, 0); + arr = b.CreateInsertValue(arr, n, 1); + arr = b.CreateInsertValue(arr, n, 2); + return {arr, at, false, true}; +} + +// ---------- operators ---------- + +static const char *opName(BinOp op) { + switch (op) { + case BinOp::Add: return "+"; + case BinOp::Sub: return "-"; + case BinOp::Mul: return "*"; + case BinOp::Div: return "/"; + case BinOp::Mod: return "%"; + case BinOp::BitAnd: return "&"; + case BinOp::BitOr: return "|"; + case BinOp::BitXor: return "^"; + case BinOp::Shl: return "<<"; + case BinOp::Shr: return ">>"; + case BinOp::Eq: return "=="; + case BinOp::NotEq: return "!="; + case BinOp::Less: return "<"; + case BinOp::LessEq: return "<="; + case BinOp::Greater: return ">"; + case BinOp::GreaterEq: return ">="; + case BinOp::And: return "&&"; + case BinOp::Or: return "||"; + } + return "?"; +} + +void Codegen::badOperands(BinOp op, const Value_ &l, const Value_ &r, Pos p) { + std::string msg = std::string("can't use '") + opName(op) + "' on " + l.type.show() + + (l.type == r.type ? "" : " and " + r.type.show()); + if (l.type.isInt() && r.type.isInt() && l.type != r.type) msg += " (convert one side, like " + l.type.show() + "(...))"; + if (l.type.kind == FType::Bool && (op == BinOp::BitAnd || op == BinOp::BitOr)) + msg += std::string(" (for true/false use '") + (op == BinOp::BitAnd ? "&&" : "||") + "')"; + else if ((l.type.isFloat() || r.type.isFloat()) && op >= BinOp::BitAnd && op <= BinOp::Shr) + msg += " (bit operators work on whole numbers only)"; + else if (op == BinOp::Add && (l.type.kind == FType::Str || r.type.kind == FType::Str)) + msg += " (turn the other side into text first: str(...))"; + else if ((l.type.kind == FType::Struct || l.type.kind == FType::Array) && (op == BinOp::Eq || op == BinOp::NotEq)) + msg += " (compare the parts one by one)"; + failAt(p.file, p.line, p.col, msg); +} + +// Bring both sides to one type: a number literal adapts to the other side, +// otherwise the smaller type grows into the bigger one. +bool Codegen::unify(Value_ &l, Value_ &r) { + if (l.type == r.type) return true; + if (l.type.isInt() && r.type.isInt()) { + auto *lc = l.literal ? dyn_cast(l.v) : nullptr; + auto *rc = r.literal ? dyn_cast(r.v) : nullptr; + if (lc && !rc && fits(lc, l.type, r.type)) { l = {intCast(l.v, l.type, r.type), r.type, true}; return true; } + if (rc && !lc && fits(rc, r.type, l.type)) { r = {intCast(r.v, r.type, l.type), l.type, true}; return true; } + if (widens(l.type, r.type)) { l = {intCast(l.v, l.type, r.type), r.type}; return true; } + if (widens(r.type, l.type)) { r = {intCast(r.v, r.type, l.type), l.type}; return true; } + return false; + } + if (l.type.isInt() && r.type.isFloat()) { l = {intToFloat(l.v, l.type, r.type), r.type, l.literal}; return true; } + if (l.type.isFloat() && r.type.isInt()) { r = {intToFloat(r.v, r.type, l.type), l.type, r.literal}; return true; } + if (l.type.isFloat() && r.type.isFloat()) { + if (l.type.kind == FType::F32) l = {b.CreateFPExt(l.v, b.getDoubleTy()), FType::F64}; + if (r.type.kind == FType::F32) r = {b.CreateFPExt(r.v, b.getDoubleTy()), FType::F64}; + return true; + } + return false; +} + +Value_ Codegen::arith(BinOp op, Value_ l, Value_ r, Pos p) { + Value_ v = arithOp(op, l, r, p); + v.literal = l.literal && r.literal && isa(v.v); + return v; +} + +Value_ Codegen::arithOp(BinOp op, Value_ l, Value_ r, Pos p) { + if (op == BinOp::Add && l.type.kind == FType::Str && r.type.kind == FType::Str) { + Value *out = tmp(Constant::getNullValue(ty(FType::Str))); + b.CreateCall(rt("finch_str_concat"), {out, tmp(l.v), tmp(r.v)}); + release(l); + release(r); + return {b.CreateLoad(ty(FType::Str), out), FType::Str, false, true}; + } + if (!unify(l, r)) badOperands(op, l, r, p); + if (l.type.isInt()) { + bool s = l.type.isSigned(); + switch (op) { + case BinOp::Add: return {b.CreateAdd(l.v, r.v), l.type}; + case BinOp::Sub: return {b.CreateSub(l.v, r.v), l.type}; + case BinOp::Mul: return {b.CreateMul(l.v, r.v), l.type}; + case BinOp::Div: + checkDivisor(l, r, p); + return {s ? b.CreateSDiv(l.v, r.v) : b.CreateUDiv(l.v, r.v), l.type}; + case BinOp::Mod: + checkDivisor(l, r, p); + return {s ? b.CreateSRem(l.v, r.v) : b.CreateURem(l.v, r.v), l.type}; + case BinOp::BitAnd: return {b.CreateAnd(l.v, r.v), l.type}; + case BinOp::BitOr: return {b.CreateOr(l.v, r.v), l.type}; + case BinOp::BitXor: return {b.CreateXor(l.v, r.v), l.type}; + case BinOp::Shl: return {b.CreateShl(l.v, r.v), l.type}; + case BinOp::Shr: return {s ? b.CreateAShr(l.v, r.v) : b.CreateLShr(l.v, r.v), l.type}; + default: break; + } + } + if (l.type.isFloat()) { + switch (op) { + case BinOp::Add: return {b.CreateFAdd(l.v, r.v), l.type}; + case BinOp::Sub: return {b.CreateFSub(l.v, r.v), l.type}; + case BinOp::Mul: return {b.CreateFMul(l.v, r.v), l.type}; + case BinOp::Div: return {b.CreateFDiv(l.v, r.v), l.type}; + case BinOp::Mod: return {b.CreateFRem(l.v, r.v), l.type}; + default: break; + } + } + badOperands(op, l, r, p); +} + +Value_ Codegen::compare(BinOp op, Value_ l, Value_ r, Pos p) { + bool equality = op == BinOp::Eq || op == BinOp::NotEq; + auto ptrLike = [](const FType &t) { return t.isPtr() || t.kind == FType::Null; }; + + if (ptrLike(l.type) || ptrLike(r.type)) { // pointers compare addresses + if (!equality || !ptrLike(l.type) || !ptrLike(r.type)) { + if (l.type.kind == FType::Str || r.type.kind == FType::Str) + failAt(p.file, p.line, p.col, "a str is never null; to check for empty text use s.len == 0"); + badOperands(op, l, r, p); + } + return {op == BinOp::Eq ? b.CreateICmpEQ(l.v, r.v) : b.CreateICmpNE(l.v, r.v), FType::Bool}; + } + + if (l.type.kind == FType::Str && r.type.kind == FType::Str) { + Value *res; + if (equality) { + Value *eq = b.CreateCall(rt("finch_str_eq"), {tmp(l.v), tmp(r.v)}); + res = op == BinOp::Eq ? b.CreateICmpNE(eq, b.getInt32(0)) : b.CreateICmpEQ(eq, b.getInt32(0)); + } else { + Value *c = b.CreateCall(rt("finch_str_cmp"), {tmp(l.v), tmp(r.v)}); + CmpInst::Predicate pr = op == BinOp::Less ? CmpInst::ICMP_SLT : op == BinOp::LessEq ? CmpInst::ICMP_SLE + : op == BinOp::Greater ? CmpInst::ICMP_SGT : CmpInst::ICMP_SGE; + res = b.CreateICmp(pr, c, b.getInt64(0)); + } + release(l); + release(r); + return {res, FType::Bool}; + } + + if (!unify(l, r)) badOperands(op, l, r, p); + if (l.type.isFloat()) { + CmpInst::Predicate pr; + switch (op) { + case BinOp::Eq: pr = CmpInst::FCMP_OEQ; break; + case BinOp::NotEq: pr = CmpInst::FCMP_UNE; break; + case BinOp::Less: pr = CmpInst::FCMP_OLT; break; + case BinOp::LessEq: pr = CmpInst::FCMP_OLE; break; + case BinOp::Greater: pr = CmpInst::FCMP_OGT; break; + default: pr = CmpInst::FCMP_OGE; break; + } + return {b.CreateFCmp(pr, l.v, r.v), FType::Bool}; + } + if (!l.type.isInt() && l.type.kind != FType::Char && l.type.kind != FType::Bool) badOperands(op, l, r, p); + if (l.type.kind == FType::Bool && !equality) badOperands(op, l, r, p); + bool s = l.type.isSigned(); + CmpInst::Predicate pr; + switch (op) { + case BinOp::Eq: pr = CmpInst::ICMP_EQ; break; + case BinOp::NotEq: pr = CmpInst::ICMP_NE; break; + case BinOp::Less: pr = s ? CmpInst::ICMP_SLT : CmpInst::ICMP_ULT; break; + case BinOp::LessEq: pr = s ? CmpInst::ICMP_SLE : CmpInst::ICMP_ULE; break; + case BinOp::Greater: pr = s ? CmpInst::ICMP_SGT : CmpInst::ICMP_UGT; break; + default: pr = s ? CmpInst::ICMP_SGE : CmpInst::ICMP_UGE; break; + } + return {b.CreateICmp(pr, l.v, r.v), FType::Bool}; +} + +// && and || only evaluate the right side when needed. +Value_ Codegen::logic(const BinaryExpr &e) { + bool isAnd = e.op == BinOp::And; + Value *l = condition(*e.lhs); + BasicBlock *from = b.GetInsertBlock(); + BasicBlock *rhsBB = newBlock(isAnd ? "and.rhs" : "or.rhs"); + BasicBlock *end = newBlock(isAnd ? "and.end" : "or.end"); + if (isAnd) b.CreateCondBr(l, rhsBB, end); + else b.CreateCondBr(l, end, rhsBB); + + b.SetInsertPoint(rhsBB); + Value *r = condition(*e.rhs); + BasicBlock *rhsEnd = b.GetInsertBlock(); + b.CreateBr(end); + + b.SetInsertPoint(end); + PHINode *phi = b.CreatePHI(b.getInt1Ty(), 2); + phi->addIncoming(b.getInt1(!isAnd), from); + phi->addIncoming(r, rhsEnd); + return {phi, FType::Bool}; +} + +Value_ Codegen::binary(const BinaryExpr &e) { + switch (e.op) { + case BinOp::And: + case BinOp::Or: return logic(e); + case BinOp::Eq: + case BinOp::NotEq: + case BinOp::Less: + case BinOp::LessEq: + case BinOp::Greater: + case BinOp::GreaterEq: { + Value_ l = expr(*e.lhs); + return compare(e.op, l, expr(*e.rhs), e.pos); + } + default: { + Value_ l = expr(*e.lhs); + return arith(e.op, l, expr(*e.rhs), e.pos); + } + } +} + +// ---------- debug info ---------- + +void Codegen::setLoc(Pos p) { + if (!di || !diFn) return; + b.SetCurrentDebugLocation(DILocation::get(ctx, p.line, p.col, diFn)); +} + +} // namespace finch + +std::unique_ptr generate(const std::vector &progs, const CImports &c, llvm::LLVMContext &ctx, + llvm::TargetMachine &tm, bool debug) { + return finch::Codegen(ctx, progs, c, tm, debug).run(); } diff --git a/src/codegen.h b/src/codegen.h index be2e51b..095abb6 100644 --- a/src/codegen.h +++ b/src/codegen.h @@ -9,6 +9,7 @@ #include +// 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. -std::unique_ptr generate(const Program &prog, const CImports &c, llvm::LLVMContext &ctx, - llvm::TargetMachine &tm); +std::unique_ptr generate(const std::vector &progs, const CImports &c, llvm::LLVMContext &ctx, + llvm::TargetMachine &tm, bool debug); diff --git a/src/codegen_builtins.cpp b/src/codegen_builtins.cpp new file mode 100644 index 0000000..bea243b --- /dev/null +++ b/src/codegen_builtins.cpp @@ -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 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 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 &args, const std::vector &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 params, bool vararg) { + FunctionCallee c = mod->getOrInsertFunction(name, FunctionType::get(ret, params, vararg)); + return cast(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 params; }; + std::map 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(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 &args, const std::vector &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 vals; + std::vector 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 &args, const std::vector &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 fields; + for (auto &f : s->fields) fields.push_back(&f); + + bool named = !names.empty() && !names[0].empty(); + std::vector 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 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 &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 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 &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 &args, Pos p) { + const Expr &e = *args[0]; + if (e.kind == ExprKind::Var) { + const std::string &n = static_cast(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 &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(*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 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 diff --git a/src/codegen_impl.h b/src/codegen_impl.h new file mode 100644 index 0000000..58a7f91 --- /dev/null +++ b/src/codegen_impl.h @@ -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 +#include +#include +#include +#include + +#include +#include +#include +#include + +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 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 vars; + std::vector cleanups; +}; + +struct Fn { + llvm::Function *llvm = nullptr; + const FnDecl *decl = nullptr; + std::string module; + std::vector borrowParam; // owning params the function never changes: no copy + llvm::Function *cThunk = nullptr; + FType ret; // resolved types + std::vector 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 fns; + std::map structs; + std::set 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 parts; // Expand: one value per eightbyte + llvm::Type *coerced = nullptr; // Expand: { parts... } for returns +}; + +class Codegen { +public: + Codegen(llvm::LLVMContext &c, const std::vector &progs, const CImports &ci, llvm::TargetMachine &tm, + bool debug); + std::unique_ptr run(); + + // ---------- state ---------- + llvm::LLVMContext &ctx; + llvm::IRBuilder<> b; + std::unique_ptr mod; + const std::vector &progs; + const CImports &cimports; + const llvm::DataLayout *dl; + + std::map modules; + std::map cStructs; + std::vector> structStore; + std::vector scopes; + std::vector 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 helpers; // drop/copy/print per type + + // debug info (-g) + bool debug; + std::unique_ptr di; + std::vector diFiles; + llvm::DICompileUnit *diUnit = nullptr; + llvm::DISubprogram *diFn = nullptr; + std::map 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 &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 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 &args, const std::vector &names, Pos p, + const std::string &shownName); + Value_ construct(StructInfo *s, const std::vector &args, const std::vector &names, Pos p); + Value_ callC(const CFunc &f, const std::vector &args, Pos p); + Value_ convert(const FType &to, const std::vector &args, Pos p, const std::string &name); + Value_ print(const std::vector &args, Pos p); + Value_ addrOf(const std::vector &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 &args, const std::vector &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 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 args, Pos p); + llvm::Function *cThunk(Fn &fn, Pos p); + + // ---------- debug info ---------- + void setLoc(Pos p); + llvm::DIType *diType(const FType &t); +}; + +} // namespace finch diff --git a/src/codegen_types.cpp b/src/codegen_types.cpp new file mode 100644 index 0000000..33b4158 --- /dev/null +++ b/src/codegen_types.cpp @@ -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(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 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(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 &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(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 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 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 diff --git a/src/error.h b/src/error.h index 3fcf29e..ee71c4d 100644 --- a/src/error.h +++ b/src/error.h @@ -3,21 +3,28 @@ #include #include #include +#include + +// Every source file the compiler has read; positions point into this list. +struct SourceFile { + std::string path; + std::string text; +}; +inline std::vector 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. -inline std::string g_file; -inline std::string g_source; - -[[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()); +[[noreturn]] inline void failAt(int file, int line, int col, const std::string &msg) { + 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()); size_t start = 0; 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 < g_source.size()) { - std::string text = g_source.substr(start, g_source.find('\n', start) - start); + if (line > 0 && start != std::string::npos && start < f.text.size()) { + std::string text = f.text.substr(start, f.text.find('\n', start) - start); std::string pad; int c = 1; // columns count characters; skip UTF-8 continuation bytes for (size_t k = 0; k < text.size() && c < col; k++) { @@ -29,3 +36,5 @@ inline std::string g_source; } std::exit(1); } + +[[noreturn]] inline void fail(int line, int col, const std::string &msg) { failAt(g_curFile, line, col, msg); } diff --git a/src/lexer.cpp b/src/lexer.cpp index 9222167..30d2874 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -9,12 +9,13 @@ static const std::unordered_map keywords = { {"else", Tok::Else}, {"while", Tok::While}, {"for", Tok::For}, {"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue}, {"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 lex(const std::string &src, const std::string &file) { - g_file = file; - g_source = src; +std::vector lex(int file) { + g_curFile = file; + const std::string &src = g_files[file].text; std::vector out; size_t i = 0; int line = 1, col = 1; @@ -126,10 +127,7 @@ std::vector lex(const std::string &src, const std::string &file) { 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::GreaterEq) : n == '>' ? two(Tok::Shr) : one(Tok::Greater); break; - case ':': - if (n != '=') fail(line, col, "unexpected ':' (did you mean ':='?)"); - two(Tok::Declare); - break; + case ':': n == '=' ? two(Tok::Declare) : one(Tok::Colon); break; case '&': n == '&' ? two(Tok::And) : one(Tok::Amp); break; case '|': n == '|' ? two(Tok::Or) : one(Tok::Pipe); break; default: { @@ -169,6 +167,9 @@ const char *tokName(Tok t) { case Tok::Null: return "'null'"; case Tok::Import: return "'import'"; 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::RParen: return "')'"; case Tok::LBrace: return "'{'"; diff --git a/src/lexer.h b/src/lexer.h index 02220fc..4404e82 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -8,10 +8,10 @@ enum class Tok { Ident, Int, Float, String, Char, // 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 - LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Dot, DotDot, Arrow, + LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot, DotDot, Arrow, // operators Plus, Minus, Star, Slash, Percent, @@ -30,5 +30,6 @@ struct Token { bool newlineBefore; // token starts a new line: ends the previous statement }; -std::vector lex(const std::string &src, const std::string &file); +// Lexes g_files[file].text. +std::vector lex(int file); const char *tokName(Tok t); diff --git a/src/main.cpp b/src/main.cpp index 86ce3e5..d120afa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,14 +1,16 @@ #include "cimport.h" #include "codegen.h" +#include "error.h" #include "lexer.h" #include "parser.h" +#include #include #include #include #include +#include #include -#include #include #include @@ -18,38 +20,90 @@ #include #include #include +#include #include -#include #include +#include #include +#include "rt_source.inc" // kRuntimeSource: runtime/finch_rt.c, embedded at build time + using namespace llvm; static void usage() { std::fprintf(stderr, "Finch " FINCH_VERSION " - small, quick, sharp.\n\n" "usage:\n" - " finch run compile and run\n" + " finch run [args...] compile and run\n" " finch build [-o name] compile to a program\n" + " finch ir show the generated LLVM IR\n" + " finch version show the version\n" "\n" " -l link a C library, same as link \"lib\" in the file\n" - " -O0 skip optimizations (to read the raw IR)\n" - " finch ir show the generated LLVM IR\n" - " finch version show the version\n"); + " -g add debug info (for gdb / lldb)\n" + " -O0 skip optimizations (to read the raw IR)\n"); 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); - if (!in) { - std::fprintf(stderr, "error: can't open '%s'\n", path.c_str()); - std::exit(1); - } + if (!in) return false; std::stringstream ss; 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 progs; + std::map 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 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) { LoopAnalysisManager lam; FunctionAnalysisManager fam; @@ -93,6 +147,8 @@ static void emitObject(Module &mod, TargetMachine *tm, const std::string &path) pm.run(mod); } +// ---------- linking ---------- + // Run a shell command; returns its exit status, everything it printed goes to `output`. static int capture(const std::string &cmd, std::string &output) { 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; } +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(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. static std::string libFlags(const std::string &lib) { std::string out; @@ -151,7 +239,6 @@ static std::string guessLib(const std::string &header) { 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()); - // group the missing functions by the header they came from std::vector>> byHeader; for (const std::string &name : missing) { auto f = c.fns.find(name); @@ -178,14 +265,34 @@ static std::string guessLib(const std::string &header) { 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 &libs, const CImports &c) { - const char *cc = std::getenv("CC"); - std::string cmd = std::string(cc ? cc : "cc") + " '" + obj + "' -o '" + exe + "' -lm"; - for (const std::string &l : libs) cmd += libFlags(l); + std::string cmd = ccName() + " '" + obj + "' '" + runtimeObject() + "' -o '" + exe + "' -lm"; + std::vector temps; + 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; int status = capture(cmd, output); std::remove(obj.c_str()); + for (const std::string &t : temps) std::remove(t.c_str()); if (status == 127) { std::fprintf(stderr, "error: no C compiler found to link with (install gcc or clang, or set CC)\n"); 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); } +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) { 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); @@ -208,26 +321,40 @@ int main(int argc, char **argv) { } if (argc < 3) usage(); std::string cmd = argv[1], file = argv[2]; + if (cmd != "run" && cmd != "build" && cmd != "ir") usage(); std::string out = stem(file); - std::vector libs; - bool optimizeCode = true; + std::vector libs, programArgs; + bool optimizeCode = true, debug = false; for (int i = 3; i < argc; 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 == "-g") debug = true; 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 (cmd == "run") programArgs.push_back(a); else usage(); } - if (cmd != "run" && cmd != "build" && cmd != "ir") usage(); - Program prog = parse(lex(readFile(file), file)); - for (const Link &l : prog.links) libs.push_back(l.lib); - size_t slash = file.find_last_of('/'); - CImports c = importHeaders(prog.imports, slash == std::string::npos ? "." : file.substr(0, slash)); + Loader loader; + loader.load(file, "", Pos{}); + std::vector allImports; + std::vector 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; TargetMachine *tm = hostMachine(); - std::unique_ptr mod = generate(prog, c, ctx, *tm); + std::unique_ptr mod = generate(loader.progs, c, ctx, *tm, debug); if (optimizeCode) optimize(*mod, tm); if (cmd == "ir") { @@ -246,7 +373,9 @@ int main(int argc, char **argv) { link(obj, out, libs, c); 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()); if (WIFSIGNALED(status)) { std::fprintf(stderr, "\nthe program crashed: %s\n", strsignal(WTERMSIG(status))); diff --git a/src/parser.cpp b/src/parser.cpp index a513a3c..be451f7 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include bool typeFromName(const std::string &name, Type &out) { @@ -29,28 +30,32 @@ bool isTypeName(const std::string &name) { class Parser { public: - explicit Parser(const std::vector &t) : toks(t) {} + Parser(const std::vector &t, int f) : toks(t), file(f) {} Program program() { Program p; while (!at(Tok::End)) { - if (at(Tok::Import)) p.imports.push_back(import()); - else if (at(Tok::Link)) p.links.push_back(link()); - else p.fns.push_back(function()); + switch (cur().kind) { + case Tok::Import: p.imports.push_back(import()); break; + 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; } private: const std::vector &toks; + int file; 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 &peekTok(size_t off = 1) const { return toks[std::min(i + off, toks.size() - 1)]; } const Token &next() { return toks[i++]; } 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 { return t.kind == Tok::Ident ? "'" + t.text + "'" : tokName(t.kind); @@ -71,27 +76,44 @@ private: 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; } - // int, u8, ptr, ptr[int], ptr[ptr[u8]] ... + // int, u8, ptr, ptr[int], []str, Point, math.Vec ... Type type() { - if (!at(Tok::Ident) || !isTypeName(cur().text)) - unexpected("(expected a type like int, float, u8, str or ptr[int])"); + if (accept(Tok::LBracket)) { + 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; - typeFromName(next().text, t); - if (t.kind == Type::Ptr && accept(Tok::LBracket)) { - t = Type::ptrTo(type()); - expect(Tok::RBracket); + if (typeFromName(name, t)) { + if (t.kind == Type::Ptr && accept(Tok::LBracket)) { + t = Type::ptrTo(type()); + 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; } - // 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 { - 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; - 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 ---------- @@ -100,9 +122,12 @@ private: Import im; im.pos = pos(); expect(Tok::Import); - 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.path = expect(Tok::String, "a header name in quotes, like \"stdio.h\"").text; + if (at(Tok::Ident)) { + im.isC = false; + 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; } @@ -111,17 +136,44 @@ private: l.pos = pos(); expect(Tok::Link); 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) fail(l.pos.line, l.pos.col, "write just the library's name: for libglfw.so that's link \"glfw\""); 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 fn; fn.pos = pos(); 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 { ... }"); - 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; expect(Tok::LParen); @@ -177,12 +229,20 @@ private: } case Tok::For: { next(); - auto s = std::make_unique(p); - s->var = expect(Tok::Ident, "a loop variable name").text; + std::string var = expect(Tok::Ident, "a loop variable name").text; expect(Tok::In); - s->from = expr(); - expect(Tok::DotDot, "'..' (like 'for i in 0..10')"); - s->to = expr(); + ExprPtr first = expr(); + if (accept(Tok::DotDot)) { // for i in 0..10 + auto s = std::make_unique(p); + s->var = var; + s->from = std::move(first); + s->to = expr(); + s->body = block(); + return s; + } + auto s = std::make_unique(p); // for x in list + s->var = var; + s->list = std::move(first); s->body = block(); return s; } @@ -194,19 +254,25 @@ private: } case Tok::Break: next(); return std::make_unique(p); case Tok::Continue: next(); return std::make_unique(p); + case Tok::Defer: { + next(); + if (at(Tok::Defer)) fail(p.line, p.col, "defer inside defer is not allowed"); + return std::make_unique(p, statement()); + } 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::Struct: fail(p.line, p.col, "structs go outside functions, at the top level of the file"); case Tok::Import: case Tok::Link: fail(p.line, p.col, "import and link go at the top of the file, outside functions"); default: break; } - // int x = 5 + // int x = 5, Point p, []int a if (atDeclaration()) { auto s = std::make_unique(p); s->hasType = true; s->type = type(); - s->name = next().text; + s->name = expect(Tok::Ident, "a variable name").text; if (accept(Tok::Assign)) s->init = expr(); return s; } @@ -223,7 +289,7 @@ private: ExprPtr e = expr(); - // x = 5, p.value += 1, ... + // x = 5, p.value += 1, a[i] = 2, ... char op = 0; switch (cur().kind) { case Tok::Assign: op = '='; break; @@ -237,8 +303,8 @@ private: default: break; } if (op && sameLine()) { - if (e->kind != ExprKind::Var && e->kind != ExprKind::Member) - fail(p.line, p.col, "only a variable or p.value can be assigned to"); + if (e->kind != ExprKind::Var && e->kind != ExprKind::Member && e->kind != ExprKind::Index) + fail(p.line, p.col, "only a variable, a field, an element or p.value can be assigned to"); auto s = std::make_unique(p); s->target = std::move(e); s->op = op; @@ -247,7 +313,8 @@ private: 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(p, std::move(e)); } @@ -355,29 +422,51 @@ private: return postfix(); } - // p.value + // p.value, a.len, a[i], a.push(x), math.sqrt(2) ExprPtr postfix() { ExprPtr e = primary(); - while (sameLine() && at(Tok::Dot)) { + while (sameLine()) { Pos p = pos(); - next(); - e = std::make_unique(p, std::move(e), expect(Tok::Ident, "a name after '.'").text); + if (accept(Tok::Dot)) { + std::string name = expect(Tok::Ident, "a name after '.'").text; + if (at(Tok::LParen) && !cur().newlineBefore) { + auto m = std::make_unique(p, std::move(e), name); + args(m->args, m->argNames); + e = std::move(m); + } else { + e = std::make_unique(p, std::move(e), name); + } + } else if (at(Tok::LBracket)) { + next(); + parenDepth++; + ExprPtr index = expr(); + parenDepth--; + expect(Tok::RBracket); + e = std::make_unique(p, std::move(e), std::move(index)); + } else { + return e; + } } return e; } - // name(arg, arg, ...) -- we are at the '(' - ExprPtr callArgs(Pos p, const std::string &name) { - auto call = std::make_unique(p, name); + // ( arg, name: arg, ... ) -- we are at the '(' + void args(std::vector &out, std::vector &names) { expect(Tok::LParen); parenDepth++; if (!at(Tok::RParen)) { - do call->args.push_back(expr()); - while (accept(Tok::Comma)); + do { + 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--; expect(Tok::RParen); - return call; } ExprPtr primary() { @@ -406,9 +495,25 @@ private: expect(Tok::RParen); return e; } + case Tok::LBracket: { // [1, 2, 3] + next(); + auto a = std::make_unique(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: { next(); - if (at(Tok::LParen) && !cur().newlineBefore) return callArgs(p, t.text); + if (at(Tok::LParen) && !cur().newlineBefore) { + auto call = std::make_unique(p, t.text); + args(call->args, call->argNames); + return call; + } if (isTypeName(t.text)) fail(p.line, p.col, "a type name alone is not a value (did you mean " + t.text + "(...)?)"); return std::make_unique(p, t.text); @@ -420,6 +525,6 @@ private: } // namespace -Program parse(const std::vector &tokens) { - return Parser(tokens).program(); +Program parse(const std::vector &tokens, int file) { + return Parser(tokens, file).program(); } diff --git a/src/parser.h b/src/parser.h index 4daab16..7769892 100644 --- a/src/parser.h +++ b/src/parser.h @@ -3,4 +3,5 @@ #include "ast.h" #include "lexer.h" -Program parse(const std::vector &tokens); +// Parses one file's tokens; `file` is its index in g_files. +Program parse(const std::vector &tokens, int file); diff --git a/tests/fail/array_compare.fn b/tests/fail/array_compare.fn new file mode 100644 index 0000000..0419cee --- /dev/null +++ b/tests/fail/array_compare.fn @@ -0,0 +1,4 @@ +// expect: compare the parts one by one +fn main() { + print([1] == [1]) +} diff --git a/tests/fail/bad_int.fn b/tests/fail/bad_int.fn new file mode 100644 index 0000000..227139e --- /dev/null +++ b/tests/fail/bad_int.fn @@ -0,0 +1,4 @@ +// expect: can't turn "12x" into int +fn main() { + print(int("12x")) +} diff --git a/tests/fail/defer_return.fn b/tests/fail/defer_return.fn new file mode 100644 index 0000000..bd61dea --- /dev/null +++ b/tests/fail/defer_return.fn @@ -0,0 +1,4 @@ +// expect: return can't be used inside defer +fn main() { + defer return +} diff --git a/tests/fail/each_readonly.fn b/tests/fail/each_readonly.fn new file mode 100644 index 0000000..2110f7d --- /dev/null +++ b/tests/fail/each_readonly.fn @@ -0,0 +1,6 @@ +// expect: the loop variable 'x' can't be changed +fn main() { + for x in [1, 2] { + x = 3 + } +} diff --git a/tests/fail/empty_array.fn b/tests/fail/empty_array.fn new file mode 100644 index 0000000..5899b6d --- /dev/null +++ b/tests/fail/empty_array.fn @@ -0,0 +1,4 @@ +// expect: an empty [] needs a type +fn main() { + a := [] +} diff --git a/tests/fail/free_value.fn b/tests/fail/free_value.fn new file mode 100644 index 0000000..cd6e6dc --- /dev/null +++ b/tests/fail/free_value.fn @@ -0,0 +1,5 @@ +// expect: arrays, str and structs are freed by Finch +fn main() { + x := 5 + free(x) +} diff --git a/tests/fail/index_range.fn b/tests/fail/index_range.fn new file mode 100644 index 0000000..0a9933e --- /dev/null +++ b/tests/fail/index_range.fn @@ -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]) +} diff --git a/tests/fail/module_missing.fn b/tests/fail/module_missing.fn new file mode 100644 index 0000000..355b801 --- /dev/null +++ b/tests/fail/module_missing.fn @@ -0,0 +1,3 @@ +// expect: can't find the module 'nothere' +import nothere +fn main() {} diff --git a/tests/fail/module_not_imported.fn b/tests/fail/module_not_imported.fn new file mode 100644 index 0000000..957d37f --- /dev/null +++ b/tests/fail/module_not_imported.fn @@ -0,0 +1,4 @@ +// expect: to use a module, add import geometry +fn main() { + v := geometry.Vec(1, 2) +} diff --git a/tests/fail/pop_empty.fn b/tests/fail/pop_empty.fn new file mode 100644 index 0000000..dcf505e --- /dev/null +++ b/tests/fail/pop_empty.fn @@ -0,0 +1,5 @@ +// expect: runtime error: pop() on an empty array +fn main() { + []int a + a.pop() +} diff --git a/tests/fail/push_type.fn b/tests/fail/push_type.fn new file mode 100644 index 0000000..4a0442b --- /dev/null +++ b/tests/fail/push_type.fn @@ -0,0 +1,5 @@ +// expect: the element must be int, but this is str +fn main() { + a := [1, 2] + a.push("x") +} diff --git a/tests/fail/str_null.fn b/tests/fail/str_null.fn new file mode 100644 index 0000000..744970f --- /dev/null +++ b/tests/fail/str_null.fn @@ -0,0 +1,6 @@ +// expect: a str is never null +fn main() { + s := "a" + if s == null { + } +} diff --git a/tests/fail/str_plus_int.fn b/tests/fail/str_plus_int.fn new file mode 100644 index 0000000..06e0734 --- /dev/null +++ b/tests/fail/str_plus_int.fn @@ -0,0 +1,4 @@ +// expect: turn the other side into text first: str(...) +fn main() { + s := "a" + 5 +} diff --git a/tests/fail/struct_count.fn b/tests/fail/struct_count.fn new file mode 100644 index 0000000..d73a2ee --- /dev/null +++ b/tests/fail/struct_count.fn @@ -0,0 +1,8 @@ +// expect: P has 2 fields, but got 1 values +struct P { + int x + int y +} +fn main() { + p := P(1) +} diff --git a/tests/fail/struct_no_field.fn b/tests/fail/struct_no_field.fn new file mode 100644 index 0000000..cdef161 --- /dev/null +++ b/tests/fail/struct_no_field.fn @@ -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) +} diff --git a/tests/fail/struct_self.fn b/tests/fail/struct_self.fn new file mode 100644 index 0000000..7216c9b --- /dev/null +++ b/tests/fail/struct_self.fn @@ -0,0 +1,5 @@ +// expect: contains itself; use ptr[A] or []A +struct A { + A inner +} +fn main() {} diff --git a/tests/run.sh b/tests/run.sh index 528783a..28f0c88 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,27 +1,45 @@ #!/usr/bin/env bash # Finch test suite. # 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: " +# MEMCHECK=1 tests/run.sh also runs every program under valgrind: no leaks, no bad memory access set -u cd "$(dirname "$0")" FINCH=${FINCH:-../build/finch} pass=0; failed=0 +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT for f in run/*.fn; do + grep -q "^fn main" "$f" || continue want="${f%.fn}.out" - got=$("$FINCH" run "$f" 2>&1) - if [ "$got" == "$(cat "$want")" ]; then - pass=$((pass + 1)) - else + input="${f%.fn}.in" + [ -f "$input" ] || input=/dev/null + got=$("$FINCH" run "$f" < "$input" 2>&1) + if [ "$got" != "$(cat "$want")" ]; then failed=$((failed + 1)) echo "FAIL $f" diff <(echo "$got") "$want" | head -20 + continue 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 for f in fail/*.fn; do expect=$(head -1 "$f" | sed -n 's|^// expect: ||p') - got=$("$FINCH" run "$f" 2>&1) + got=$("$FINCH" run "$f" < /dev/null 2>&1) status=$? if [ $status -ne 0 ] && [[ "$got" == *"$expect"* ]]; then pass=$((pass + 1)) diff --git a/tests/run/abi.c b/tests/run/abi.c new file mode 100644 index 0000000..17d9cd9 --- /dev/null +++ b/tests/run/abi.c @@ -0,0 +1,17 @@ +#include "abi.h" +#include +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; } diff --git a/tests/run/abi.h b/tests/run/abi.h new file mode 100644 index 0000000..d75ca28 --- /dev/null +++ b/tests/run/abi.h @@ -0,0 +1,25 @@ +#include +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); diff --git a/tests/run/c_structs.fn b/tests/run/c_structs.fn new file mode 100644 index 0000000..592adad --- /dev/null +++ b/tests/run/c_structs.fn @@ -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))) +} diff --git a/tests/run/c_structs.out b/tests/run/c_structs.out new file mode 100644 index 0000000..7615e20 --- /dev/null +++ b/tests/run/c_structs.out @@ -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 diff --git a/tests/run/defer.fn b/tests/run/defer.fn new file mode 100644 index 0000000..763551e --- /dev/null +++ b/tests/run/defer.fn @@ -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) + } +} diff --git a/tests/run/defer.out b/tests/run/defer.out new file mode 100644 index 0000000..d5762c5 --- /dev/null +++ b/tests/run/defer.out @@ -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 diff --git a/tests/run/files.fn b/tests/run/files.fn new file mode 100644 index 0000000..1f7ae5e --- /dev/null +++ b/tests/run/files.fn @@ -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")) +} diff --git a/tests/run/files.out b/tests/run/files.out new file mode 100644 index 0000000..f9fc6b1 --- /dev/null +++ b/tests/run/files.out @@ -0,0 +1,3 @@ +true true +["line one", "line two"] 2 +false diff --git a/tests/run/geometry.fn b/tests/run/geometry.fn new file mode 100644 index 0000000..6a89699 --- /dev/null +++ b/tests/run/geometry.fn @@ -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)) +} diff --git a/tests/run/input.fn b/tests/run/input.fn new file mode 100644 index 0000000..44cf4ba --- /dev/null +++ b/tests/run/input.fn @@ -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) +} diff --git a/tests/run/input.in b/tests/run/input.in new file mode 100644 index 0000000..3a39e16 --- /dev/null +++ b/tests/run/input.in @@ -0,0 +1,2 @@ +Ola +41 diff --git a/tests/run/input.out b/tests/run/input.out new file mode 100644 index 0000000..5af3e87 --- /dev/null +++ b/tests/run/input.out @@ -0,0 +1,2 @@ +Name? Age? Hi Ola next year you will be 42 +rest: 0 diff --git a/tests/run/memory.fn b/tests/run/memory.fn new file mode 100644 index 0000000..f569fc9 --- /dev/null +++ b/tests/run/memory.fn @@ -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) +} diff --git a/tests/run/memory.out b/tests/run/memory.out new file mode 100644 index 0000000..d72aabc --- /dev/null +++ b/tests/run/memory.out @@ -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"] diff --git a/tests/run/modules.fn b/tests/run/modules.fn new file mode 100644 index 0000000..cae2d14 --- /dev/null +++ b/tests/run/modules.fn @@ -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) +} diff --git a/tests/run/modules.out b/tests/run/modules.out new file mode 100644 index 0000000..2886aed --- /dev/null +++ b/tests/run/modules.out @@ -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 diff --git a/tests/run/ptr_value_field.fn b/tests/run/ptr_value_field.fn new file mode 100644 index 0000000..56e288e --- /dev/null +++ b/tests/run/ptr_value_field.fn @@ -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) +} diff --git a/tests/run/ptr_value_field.out b/tests/run/ptr_value_field.out new file mode 100644 index 0000000..9965f88 --- /dev/null +++ b/tests/run/ptr_value_field.out @@ -0,0 +1,4 @@ +1 2 +10 +Box(size: 3) 3 +Box(size: 7) diff --git a/tests/run/shapes.fn b/tests/run/shapes.fn new file mode 100644 index 0000000..93d7b1b --- /dev/null +++ b/tests/run/shapes.fn @@ -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 +} diff --git a/tests/run/structs_arrays_strings.fn b/tests/run/structs_arrays_strings.fn new file mode 100644 index 0000000..93cf46d --- /dev/null +++ b/tests/run/structs_arrays_strings.fn @@ -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]) +} diff --git a/tests/run/structs_arrays_strings.out b/tests/run/structs_arrays_strings.out new file mode 100644 index 0000000..a923389 --- /dev/null +++ b/tests/run/structs_arrays_strings.out @@ -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