Finch 1.0.0

A small, fast programming language on LLVM with C-like syntax.

- Lexer, recursive-descent parser, single-pass type checker + LLVM IR codegen, O2 pipeline
- Types: int/float/bool/char/str, sized i8..u64/f32/f64, pointers ptr[T] with addr()/.value/null
- Functions (fn ... -> T), if/else, while, for..in, break/continue, Go-style operator precedence
- Implicit conversions only when lossless; literals adapt to their target type
- Runtime checks for division by zero, MIN/-1 and null .value
- C interop: import "header.h" via libclang, link "lib" with pkg-config, link-error advice
- Human-readable errors with source excerpts
- Test suite (34 tests), examples, docs for beginners/technicians/engineers in English and Polish

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-08 17:10:45 +02:00
commit 0591cd20f3
73 changed files with 8066 additions and 0 deletions

235
src/ast.h Normal file
View file

@ -0,0 +1,235 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
struct Type {
enum Kind { Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64, Str, Ptr, Null };
Kind kind = Void;
std::shared_ptr<Type> elem; // what a Ptr points to; null for the untyped `ptr`
Type() = default;
Type(Kind k) : kind(k) {}
static Type ptrTo(const Type &t) {
Type p(Ptr);
p.elem = std::make_shared<Type>(t);
return p;
}
bool isInt() const { return kind >= I8 && kind <= U64; }
bool isSigned() const { return kind >= I8 && kind <= I64; }
bool isUnsigned() const { return kind >= U8 && kind <= U64; }
bool isFloat() const { return kind == F32 || kind == F64; }
bool isNumber() const { return isInt() || isFloat(); }
bool isPtr() const { return kind == Ptr; }
int bits() const {
switch (kind) {
case Bool: return 1;
case Char: case I8: case U8: return 8;
case I16: case U16: return 16;
case I32: case U32: case F32: return 32;
default: return 64;
}
}
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;
}
bool operator!=(const Type &o) const { return !(*this == o); }
std::string name() const {
switch (kind) {
case Void: return "nothing";
case Bool: return "bool";
case Char: return "char";
case I8: return "i8";
case I16: return "i16";
case I32: return "i32";
case I64: return "int";
case U8: return "u8";
case U16: return "u16";
case U32: return "u32";
case U64: return "u64";
case F32: return "f32";
case F64: return "float";
case Str: return "str";
case Ptr: return elem ? "ptr[" + elem->name() + "]" : "ptr";
case Null: return "null";
}
return "?";
}
};
// Built-in type names. `int` is i64 and `float` is f64: the everyday defaults.
bool typeFromName(const std::string &name, Type &out);
struct Pos {
int line = 0, col = 0;
};
// ---------- expressions ----------
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member };
struct Expr {
ExprKind kind;
Pos pos;
Expr(ExprKind k, Pos p) : kind(k), pos(p) {}
virtual ~Expr() = default;
};
using ExprPtr = std::unique_ptr<Expr>;
struct IntExpr : Expr {
long long value;
IntExpr(Pos p, long long v) : Expr(ExprKind::Int, p), value(v) {}
};
struct FloatExpr : Expr {
double value;
FloatExpr(Pos p, double v) : Expr(ExprKind::Float, p), value(v) {}
};
struct BoolExpr : Expr {
bool value;
BoolExpr(Pos p, bool v) : Expr(ExprKind::Bool, p), value(v) {}
};
struct CharExpr : Expr {
char value;
CharExpr(Pos p, char v) : Expr(ExprKind::Char, p), value(v) {}
};
struct StrExpr : Expr {
std::string value;
StrExpr(Pos p, std::string v) : Expr(ExprKind::Str, p), value(std::move(v)) {}
};
struct NullExpr : Expr {
explicit NullExpr(Pos p) : Expr(ExprKind::Null, p) {}
};
struct VarExpr : Expr {
std::string name;
VarExpr(Pos p, std::string n) : Expr(ExprKind::Var, p), name(std::move(n)) {}
};
struct UnaryExpr : Expr {
char op; // '-', '!' or '~'
ExprPtr operand;
UnaryExpr(Pos p, char o, ExprPtr e) : Expr(ExprKind::Unary, p), op(o), operand(std::move(e)) {}
};
enum class BinOp {
Add, Sub, Mul, Div, Mod,
BitAnd, BitOr, BitXor, Shl, Shr,
Eq, NotEq, Less, LessEq, Greater, GreaterEq,
And, Or,
};
struct BinaryExpr : Expr {
BinOp op;
ExprPtr lhs, rhs;
BinaryExpr(Pos p, BinOp o, ExprPtr l, ExprPtr r)
: Expr(ExprKind::Binary, p), op(o), lhs(std::move(l)), rhs(std::move(r)) {}
};
struct CallExpr : Expr {
std::string callee;
std::vector<ExprPtr> args;
CallExpr(Pos p, std::string c) : Expr(ExprKind::Call, p), callee(std::move(c)) {}
};
struct MemberExpr : Expr { // obj.field (for now only p.value)
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)) {}
};
// ---------- statements ----------
enum class StmtKind { VarDecl, Assign, Expr, If, While, For, Return, Break, Continue, Block };
struct Stmt {
StmtKind kind;
Pos pos;
Stmt(StmtKind k, Pos p) : kind(k), pos(p) {}
virtual ~Stmt() = default;
};
using StmtPtr = std::unique_ptr<Stmt>;
struct BlockStmt : Stmt {
std::vector<StmtPtr> body;
explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {}
};
using BlockPtr = std::unique_ptr<BlockStmt>;
struct VarDeclStmt : Stmt {
bool hasType; // `int x = 5` vs `x := 5`
Type type;
std::string name;
ExprPtr init; // may be null only when hasType
VarDeclStmt(Pos p) : Stmt(StmtKind::VarDecl, p) {}
};
struct AssignStmt : Stmt {
ExprPtr target; // a variable or p.value
char op; // '=', '+', '-', '*', '/', '%'
ExprPtr value;
AssignStmt(Pos p) : Stmt(StmtKind::Assign, p) {}
};
struct ExprStmt : Stmt {
ExprPtr expr;
ExprStmt(Pos p, ExprPtr e) : Stmt(StmtKind::Expr, p), expr(std::move(e)) {}
};
struct IfStmt : Stmt {
ExprPtr cond;
BlockPtr then;
StmtPtr otherwise; // null, BlockStmt, or IfStmt (else if)
IfStmt(Pos p) : Stmt(StmtKind::If, p) {}
};
struct WhileStmt : Stmt {
ExprPtr cond;
BlockPtr body;
WhileStmt(Pos p) : Stmt(StmtKind::While, p) {}
};
struct ForStmt : Stmt {
std::string var;
ExprPtr from, to; // for var in from..to (to is exclusive)
BlockPtr body;
ForStmt(Pos p) : Stmt(StmtKind::For, p) {}
};
struct ReturnStmt : Stmt {
ExprPtr value; // may be null
ReturnStmt(Pos p) : Stmt(StmtKind::Return, p) {}
};
struct BreakStmt : Stmt {
explicit BreakStmt(Pos p) : Stmt(StmtKind::Break, p) {}
};
struct ContinueStmt : Stmt {
explicit ContinueStmt(Pos p) : Stmt(StmtKind::Continue, p) {}
};
// ---------- top level ----------
struct Param {
Type type;
std::string name;
Pos pos;
};
struct FnDecl {
Pos pos;
std::string name;
Type ret; // Void when there is no `-> type`
std::vector<Param> params;
BlockPtr body;
};
struct Import {
Pos pos;
std::string path; // "stdio.h" for C headers
};
struct Link {
Pos pos;
std::string lib; // "glfw" -> libglfw.so
};
struct Program {
std::vector<Import> imports;
std::vector<Link> links;
std::vector<FnDecl> fns;
};

246
src/cimport.cpp Normal file
View file

@ -0,0 +1,246 @@
#include "cimport.h"
#include "error.h"
#include <clang-c/Index.h>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace {
// Ask the system C compiler where it looks for headers, so we find the same stdio.h it would.
std::vector<std::string> systemIncludeDirs() {
const char *cc = std::getenv("CC");
std::string cmd = std::string(cc ? cc : "cc") + " -E -v -x c /dev/null 2>&1";
std::vector<std::string> dirs;
FILE *p = popen(cmd.c_str(), "r");
if (!p) return dirs;
char line[4096];
bool inList = false;
while (std::fgets(line, sizeof line, p)) {
std::string l = line;
if (l.rfind("#include <...> search starts here:", 0) == 0) { inList = true; continue; }
if (l.rfind("End of search list.", 0) == 0) break;
if (!inList) continue;
size_t a = l.find_first_not_of(' ');
size_t z = l.find_last_not_of(" \n");
if (a == std::string::npos) continue;
std::string d = l.substr(a, z - a + 1);
if (d.size() > 20 && d.compare(d.size() - 20, 20, " (framework directory)") == 0) continue;
dirs.push_back(d);
}
pclose(p);
return dirs;
}
std::string str(CXString s) {
std::string out = clang_getCString(s) ? clang_getCString(s) : "";
clang_disposeString(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;
};
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
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);
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<std::string> parts;
for (unsigned k = 1; k < n; k++) { // token 0 is the macro's name
if (clang_getTokenKind(toks[k]) == CXToken_Comment) continue;
parts.push_back(str(clang_getTokenSpelling(tu, toks[k])));
}
clang_disposeTokens(tu, toks, n);
while (parts.size() >= 2 && parts.front() == "(" && parts.back() == ")") {
parts.erase(parts.begin());
parts.pop_back();
}
bool neg = false;
if (parts.size() == 2 && parts[0] == "-") {
neg = true;
parts.erase(parts.begin());
}
if (parts.size() != 1) return false;
const std::string &lit = parts[0];
if (lit.empty() || !(std::isdigit((unsigned char)lit[0]) || lit[0] == '.')) return false;
bool isFloat = lit.find_first_of(".eE") != std::string::npos && lit.rfind("0x", 0) != 0;
char *end;
if (isFloat) {
out.type = Type::F64;
out.f = std::strtod(lit.c_str(), &end);
if (neg) out.f = -out.f;
} else {
out.type = Type::I64;
out.i = (long long)std::strtoull(lit.c_str(), &end, 0); // C rules: 0x.., 0755
if (neg) out.i = -out.i;
}
for (; *end; end++) // allow suffixes: 10UL, 1.0f
if (!std::strchr("uUlLfF", *end)) return false;
return true;
}
struct Visit {
CImports *out;
CXTranslationUnit tu;
std::string header;
};
CXChildVisitResult visit(CXCursor c, CXCursor, CXClientData data) {
Visit &v = *static_cast<Visit *>(data);
CImports &out = *v.out;
std::string name = str(clang_getCursorSpelling(c));
switch (clang_getCursorKind(c)) {
case CXCursor_FunctionDecl: {
if (out.fns.count(name)) break;
CFunc f;
f.name = name;
f.header = v.header;
if (clang_Cursor_getStorageClass(c) == CX_SC_Static) {
f.unsupported = "is a static inline C function (it has no code to link against)";
out.fns[name] = f;
break;
}
CXType ft = clang_getCanonicalType(clang_getCursorType(c));
if (ft.kind == CXType_FunctionNoProto) {
f.unsupported = "has no parameter list in its C declaration";
out.fns[name] = f;
break;
}
std::string why;
if (!mapType(clang_getResultType(ft), f.ret, why)) f.unsupported = why;
int n = clang_getNumArgTypes(ft);
for (int k = 0; k < n && f.unsupported.empty(); k++) {
Type t;
if (!mapType(clang_getArgType(ft, k), t, why)) f.unsupported = why;
f.params.push_back(t);
}
f.variadic = clang_isFunctionTypeVariadic(ft);
out.fns[name] = f;
break;
}
case CXCursor_VarDecl: {
if (clang_Cursor_getStorageClass(c) != CX_SC_Extern || out.globals.count(name)) break;
CGlobal g;
std::string why;
if (mapType(clang_getCursorType(c), g.type, why)) out.globals[name] = g;
break;
}
case CXCursor_EnumDecl:
clang_visitChildren(c, visit, data);
break;
case CXCursor_EnumConstantDecl: {
CConst k;
k.type = Type::I64;
k.i = clang_getEnumConstantDeclValue(c);
out.consts.emplace(name, k);
break;
}
case CXCursor_MacroDefinition: {
if (clang_Cursor_isMacroBuiltin(c) || clang_Cursor_isMacroFunctionLike(c)) break;
CConst k;
if (macroValue(v.tu, c, k)) out.consts.emplace(name, k);
break;
}
default: break;
}
return CXChildVisit_Continue;
}
} // namespace
CImports importHeaders(const std::vector<Import> &imports, const std::string &dir) {
CImports out;
if (imports.empty()) return out;
std::vector<std::string> args = {"-x", "c", "-std=gnu11", "-I" + dir};
for (const std::string &d : systemIncludeDirs()) args.push_back("-isystem" + d);
std::vector<const char *> argv;
for (const std::string &a : args) argv.push_back(a.c_str());
CXIndex index = clang_createIndex(0, 0);
for (const Import &im : imports) {
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 + "'");
for (unsigned k = 0; k < clang_getNumDiagnostics(tu); k++) {
CXDiagnostic d = clang_getDiagnostic(tu, k);
if (clang_getDiagnosticSeverity(d) >= CXDiagnostic_Error) {
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);
}
clang_disposeDiagnostic(d);
}
Visit v{&out, tu, im.path};
clang_visitChildren(clang_getTranslationUnitCursor(tu), visit, &v);
clang_disposeTranslationUnit(tu);
}
clang_disposeIndex(index);
return out;
}

37
src/cimport.h Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include "ast.h"
#include <string>
#include <unordered_map>
#include <vector>
// What Finch learned from `import "something.h"`.
struct CFunc {
std::string name;
std::string header; // the import it came from
Type ret;
std::vector<Type> params;
bool variadic = false;
std::string unsupported; // non-empty: why Finch can't call it (yet)
};
struct CConst { // enum values and simple #define numbers
Type type;
long long i = 0;
double f = 0;
};
struct CGlobal { // extern variables like stdout
Type type;
};
struct CImports {
std::unordered_map<std::string, CFunc> fns;
std::unordered_map<std::string, CConst> consts;
std::unordered_map<std::string, CGlobal> globals;
};
// `dir` is the folder of the .fn file, searched first for "local.h" headers.
CImports importHeaders(const std::vector<Import> &imports, const std::string &dir);

887
src/codegen.cpp Normal file
View file

@ -0,0 +1,887 @@
#include "codegen.h"
#include "error.h"
#include <llvm/IR/Constants.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/Verifier.h>
#include <llvm/Support/raw_ostream.h>
#include <unordered_map>
using namespace llvm;
using FType = ::Type; // Finch types (llvm::Type is the LLVM one)
namespace {
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
};
struct Var {
AllocaInst *slot;
FType type;
bool readonly; // loop variables
};
struct Fn {
Function *llvm;
const FnDecl *decl;
};
struct Loop {
BasicBlock *continueTo, *breakTo;
};
struct Place { // something that can be assigned to: a variable or p.value
Value *addr;
FType type;
};
class Codegen {
public:
Codegen(LLVMContext &c, const CImports &ci, TargetMachine &tm)
: ctx(c), b(c), mod(std::make_unique<Module>("finch", c)), cimports(ci) {
mod->setTargetTriple(tm.getTargetTriple());
mod->setDataLayout(tm.createDataLayout());
}
std::unique_ptr<Module> 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);
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);
}
private:
LLVMContext &ctx;
IRBuilder<> b;
std::unique_ptr<Module> mod;
const CImports &cimports;
std::unordered_map<std::string, Fn> fns;
std::vector<std::unordered_map<std::string, Var>> scopes;
std::vector<Loop> loops;
const FnDecl *curFn = nullptr;
// ---------- 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;
}
Constant *zero(const FType &t) {
if (t.kind == FType::Str) return cast<Constant>(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<ConstantInt>(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<llvm::Type *> params, bool vararg = false) {
return cast<Function>(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<llvm::Type *> 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<const BlockStmt &>(s)); break;
case StmtKind::VarDecl: varDecl(static_cast<const VarDeclStmt &>(s)); break;
case StmtKind::Assign: assign(static_cast<const AssignStmt &>(s)); break;
case StmtKind::Expr: expr(*static_cast<const ExprStmt &>(s).expr); break;
case StmtKind::If: ifStmt(static_cast<const IfStmt &>(s)); break;
case StmtKind::While: whileStmt(static_cast<const WhileStmt &>(s)); break;
case StmtKind::For: forStmt(static_cast<const ForStmt &>(s)); break;
case StmtKind::Return: returnStmt(static_cast<const ReturnStmt &>(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);
break;
}
}
}
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);
}
// Where does `x` or `p.value` live in memory?
Place place(const Expr &e) {
if (e.kind == ExprKind::Var) {
auto &v = static_cast<const VarExpr &>(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<const MemberExpr &>(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<AllocaInst>(obj.v) && !isa<GlobalValue>(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");
}
void assign(const AssignStmt &s) {
if (s.target->kind == ExprKind::Var) {
auto &name = static_cast<const VarExpr &>(*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<const VarExpr &>(*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<ConstantInt>(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<const IntExpr &>(e).value), FType::I64, true};
case ExprKind::Float: return {ConstantFP::get(b.getDoubleTy(), static_cast<const FloatExpr &>(e).value), FType::F64, true};
case ExprKind::Bool: return {b.getInt1(static_cast<const BoolExpr &>(e).value), FType::Bool};
case ExprKind::Char: return {b.getInt8(static_cast<const CharExpr &>(e).value), FType::Char};
case ExprKind::Str: return {b.CreateGlobalString(static_cast<const StrExpr &>(e).value, "str"), FType::Str};
case ExprKind::Null: return {ConstantPointerNull::get(b.getPtrTy()), FType::Null};
case ExprKind::Var: {
auto &v = static_cast<const VarExpr &>(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<const UnaryExpr &>(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<const BinaryExpr &>(e));
case ExprKind::Call: return call(static_cast<const CallExpr &>(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<ConstantInt>(l.v) : nullptr;
auto *rc = r.literal ? dyn_cast<ConstantInt>(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_ arith(BinOp op, Value_ l, Value_ r, Pos p) {
Value_ v = arithOp(op, l, r, p);
v.literal = l.literal && r.literal && isa<Constant>(v.v);
return v;
}
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;
}
}
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<ConstantInt>(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<std::string, const char *> known = {
{"printf", "stdio.h"}, {"puts", "stdio.h"}, {"putchar", "stdio.h"}, {"scanf", "stdio.h"},
{"fopen", "stdio.h"}, {"getchar", "stdio.h"}, {"sqrt", "math.h"}, {"pow", "math.h"},
{"sin", "math.h"}, {"cos", "math.h"}, {"floor", "math.h"}, {"fabs", "math.h"},
{"malloc", "stdlib.h"}, {"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<Value *> 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<llvm::Type *> 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<Value *> 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<const VarExpr &>(e).name);
if (v && v->readonly) fail(e.pos.line, e.pos.col, "can't take the address of a loop variable");
}
Place pl = place(e);
return {pl.addr, FType::ptrTo(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<Value *> 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};
}
// 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());
}
};
} // namespace
std::unique_ptr<Module> generate(const Program &prog, const CImports &c, LLVMContext &ctx, TargetMachine &tm) {
return Codegen(ctx, c, tm).run(prog);
}

14
src/codegen.h Normal file
View file

@ -0,0 +1,14 @@
#pragma once
#include "ast.h"
#include "cimport.h"
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
#include <llvm/Target/TargetMachine.h>
#include <memory>
// `tm` describes the machine we compile for (sizes, alignment), which shapes the IR.
std::unique_ptr<llvm::Module> generate(const Program &prog, const CImports &c, llvm::LLVMContext &ctx,
llvm::TargetMachine &tm);

31
src/error.h Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <cstdio>
#include <cstdlib>
#include <string>
// 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());
size_t start = 0;
for (int l = 1; l < line && start != std::string::npos; l++) {
start = g_source.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);
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++) {
if (((unsigned char)text[k] & 0xC0) == 0x80) continue;
pad += text[k] == '\t' ? '\t' : ' ';
c++;
}
std::fprintf(stderr, "%5d | %s\n | %s^\n", line, text.c_str(), pad.c_str());
}
std::exit(1);
}

211
src/lexer.cpp Normal file
View file

@ -0,0 +1,211 @@
#include "lexer.h"
#include "error.h"
#include <cctype>
#include <unordered_map>
static const std::unordered_map<std::string, Tok> keywords = {
{"fn", Tok::Fn}, {"return", Tok::Return}, {"if", Tok::If},
{"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},
};
std::vector<Token> lex(const std::string &src, const std::string &file) {
g_file = file;
g_source = src;
std::vector<Token> out;
size_t i = 0;
int line = 1, col = 1;
bool newline = true;
auto advance = [&]() {
if (src[i] == '\n') { line++; col = 1; newline = true; }
else if (((unsigned char)src[i] & 0xC0) != 0x80) col++; // count characters, not UTF-8 bytes
i++;
};
auto peek = [&](size_t off = 0) -> char {
return i + off < src.size() ? src[i + off] : '\0';
};
while (i < src.size()) {
char c = src[i];
if (std::isspace((unsigned char)c)) { advance(); continue; }
if (c == '/' && peek(1) == '/') {
while (i < src.size() && src[i] != '\n') advance();
continue;
}
if (c == '/' && peek(1) == '*') {
int l = line, cl = col;
advance(); advance();
while (i < src.size() && !(src[i] == '*' && peek(1) == '/')) advance();
if (i >= src.size()) fail(l, cl, "comment is never closed (missing */)");
advance(); advance();
continue;
}
Token t{Tok::End, "", line, col, newline};
newline = false;
if (std::isalpha((unsigned char)c) || c == '_') {
size_t start = i;
while (std::isalnum((unsigned char)peek()) || peek() == '_') advance();
t.text = src.substr(start, i - start);
auto kw = keywords.find(t.text);
t.kind = kw != keywords.end() ? kw->second : Tok::Ident;
} else if (std::isdigit((unsigned char)c)) {
size_t start = i;
t.kind = Tok::Int;
if (c == '0' && (peek(1) == 'x' || peek(1) == 'X')) { // 0xFF
advance(); advance();
while (std::isxdigit((unsigned char)peek()) || peek() == '_') advance();
if (i - start == 2) fail(t.line, t.col, "'0x' needs hex digits after it, like 0xFF");
} else {
while (std::isdigit((unsigned char)peek()) || peek() == '_') advance();
// "1..5" is a range, not the float "1."
if (peek() == '.' && std::isdigit((unsigned char)peek(1))) {
t.kind = Tok::Float;
advance();
while (std::isdigit((unsigned char)peek()) || peek() == '_') advance();
}
}
for (size_t k = start; k < i; k++)
if (src[k] != '_') t.text += src[k];
} else if (c == '"' || c == '\'') {
char quote = c;
t.kind = quote == '"' ? Tok::String : Tok::Char;
advance();
while (peek() != quote) {
if (peek() == '\0' || peek() == '\n')
fail(t.line, t.col, quote == '"' ? "string is never closed (missing \")"
: "char is never closed (missing ')");
char ch = peek();
advance();
if (ch == '\\') {
char e = peek();
advance();
switch (e) {
case 'n': ch = '\n'; break;
case 't': ch = '\t'; break;
case 'r': ch = '\r'; break;
case '0': ch = '\0'; break;
case '\\': ch = '\\'; break;
case '"': ch = '"'; break;
case '\'': ch = '\''; break;
default: fail(line, col - 1, std::string("unknown escape \\") + e);
}
}
t.text += ch;
}
advance();
if (t.kind == Tok::Char && t.text.size() != 1)
fail(t.line, t.col, "a char holds exactly one character, like 'a'");
} else {
char n = peek(1);
auto two = [&](Tok k) { t.kind = k; advance(); advance(); };
auto one = [&](Tok k) { t.kind = k; advance(); };
switch (c) {
case '(': one(Tok::LParen); break;
case ')': one(Tok::RParen); break;
case '{': one(Tok::LBrace); break;
case '}': one(Tok::RBrace); break;
case '[': one(Tok::LBracket); break;
case ']': one(Tok::RBracket); break;
case ',': one(Tok::Comma); break;
case '%': n == '=' ? two(Tok::PercentEq) : one(Tok::Percent); break;
case '^': one(Tok::Caret); break;
case '~': one(Tok::Tilde); break;
case '.': n == '.' ? two(Tok::DotDot) : one(Tok::Dot); break;
case '+': n == '=' ? two(Tok::PlusEq) : one(Tok::Plus); break;
case '-': n == '=' ? two(Tok::MinusEq) : n == '>' ? two(Tok::Arrow) : one(Tok::Minus); break;
case '*': n == '=' ? two(Tok::StarEq) : one(Tok::Star); break;
case '/': n == '=' ? two(Tok::SlashEq) : one(Tok::Slash); break;
case '=': n == '=' ? two(Tok::Eq) : one(Tok::Assign); break;
case '!': n == '=' ? two(Tok::NotEq) : one(Tok::Not); break;
case '<': n == '=' ? two(Tok::LessEq) : n == '<' ? two(Tok::Shl) : one(Tok::Less); break;
case '>': n == '=' ? two(Tok::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::And) : one(Tok::Amp); break;
case '|': n == '|' ? two(Tok::Or) : one(Tok::Pipe); break;
default: {
// show the whole UTF-8 character, not just its first byte
size_t len = (c & 0xE0) == 0xC0 ? 2 : (c & 0xF0) == 0xE0 ? 3 : (c & 0xF8) == 0xF0 ? 4 : 1;
std::string ch = src.substr(i, len);
std::string hint = (unsigned char)c >= 0x80 ? " (names use only a-z, A-Z, 0-9 and _; other letters only go inside \"text\")" : "";
fail(line, col, "unexpected character '" + ch + "'" + hint);
}
}
}
out.push_back(t);
}
out.push_back({Tok::End, "", line, col, true});
return out;
}
const char *tokName(Tok t) {
switch (t) {
case Tok::End: return "end of file";
case Tok::Ident: return "a name";
case Tok::Int: return "a number";
case Tok::Float: return "a number";
case Tok::String: return "a string";
case Tok::Char: return "a char";
case Tok::Fn: return "'fn'";
case Tok::Return: return "'return'";
case Tok::If: return "'if'";
case Tok::Else: return "'else'";
case Tok::While: return "'while'";
case Tok::For: return "'for'";
case Tok::In: return "'in'";
case Tok::Break: return "'break'";
case Tok::Continue: return "'continue'";
case Tok::True: return "'true'";
case Tok::False: return "'false'";
case Tok::Null: return "'null'";
case Tok::Import: return "'import'";
case Tok::Link: return "'link'";
case Tok::LParen: return "'('";
case Tok::RParen: return "')'";
case Tok::LBrace: return "'{'";
case Tok::RBrace: return "'}'";
case Tok::LBracket: return "'['";
case Tok::RBracket: return "']'";
case Tok::Comma: return "','";
case Tok::Dot: return "'.'";
case Tok::Arrow: return "'->'";
case Tok::DotDot: return "'..'";
case Tok::Plus: return "'+'";
case Tok::Minus: return "'-'";
case Tok::Star: return "'*'";
case Tok::Slash: return "'/'";
case Tok::Percent: return "'%'";
case Tok::Amp: return "'&'";
case Tok::Pipe: return "'|'";
case Tok::Caret: return "'^'";
case Tok::Tilde: return "'~'";
case Tok::Shl: return "'<<'";
case Tok::Shr: return "'>>'";
case Tok::PercentEq: return "'%='";
case Tok::Assign: return "'='";
case Tok::Declare: return "':='";
case Tok::PlusEq: return "'+='";
case Tok::MinusEq: return "'-='";
case Tok::StarEq: return "'*='";
case Tok::SlashEq: return "'/='";
case Tok::Eq: return "'=='";
case Tok::NotEq: return "'!='";
case Tok::Less: return "'<'";
case Tok::LessEq: return "'<='";
case Tok::Greater: return "'>'";
case Tok::GreaterEq: return "'>='";
case Tok::And: return "'&&'";
case Tok::Or: return "'||'";
case Tok::Not: return "'!'";
}
return "?";
}

34
src/lexer.h Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include <string>
#include <vector>
enum class Tok {
End,
Ident, Int, Float, String, Char,
// keywords
Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link,
// punctuation
LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Dot, DotDot, Arrow,
// operators
Plus, Minus, Star, Slash, Percent,
Amp, Pipe, Caret, Tilde, Shl, Shr, // & | ^ ~ << >>
Assign, Declare, // = :=
PlusEq, MinusEq, StarEq, SlashEq, PercentEq, // += -= *= /= %=
Eq, NotEq, Less, LessEq, Greater, GreaterEq,
And, Or, Not,
};
struct Token {
Tok kind;
std::string text;
int line;
int col;
bool newlineBefore; // token starts a new line: ends the previous statement
};
std::vector<Token> lex(const std::string &src, const std::string &file);
const char *tokName(Tok t);

258
src/main.cpp Normal file
View file

@ -0,0 +1,258 @@
#include "cimport.h"
#include "codegen.h"
#include "lexer.h"
#include "parser.h"
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/MC/TargetRegistry.h>
#include <llvm/Passes/PassBuilder.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Config/llvm-config.h>
#include <llvm/Target/TargetMachine.h>
#include <llvm/TargetParser/Host.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <sys/wait.h>
#include <string>
#include <vector>
using namespace llvm;
static void usage() {
std::fprintf(stderr,
"Finch " FINCH_VERSION " - small, quick, sharp.\n\n"
"usage:\n"
" finch run <file.fn> compile and run\n"
" finch build <file.fn> [-o name] compile to a program\n"
"\n"
" -l <lib> link a C library, same as link \"lib\" in the file\n"
" -O0 skip optimizations (to read the raw IR)\n"
" finch ir <file.fn> show the generated LLVM IR\n"
" finch version show the version\n");
std::exit(1);
}
static std::string readFile(const std::string &path) {
std::ifstream in(path);
if (!in) {
std::fprintf(stderr, "error: can't open '%s'\n", path.c_str());
std::exit(1);
}
std::stringstream ss;
ss << in.rdbuf();
return ss.str();
}
static void optimize(Module &mod, TargetMachine *tm) {
LoopAnalysisManager lam;
FunctionAnalysisManager fam;
CGSCCAnalysisManager cgam;
ModuleAnalysisManager mam;
PassBuilder pb(tm);
pb.registerModuleAnalyses(mam);
pb.registerCGSCCAnalyses(cgam);
pb.registerFunctionAnalyses(fam);
pb.registerLoopAnalyses(lam);
pb.crossRegisterProxies(lam, fam, cgam, mam);
pb.buildPerModuleDefaultPipeline(OptimizationLevel::O2).run(mod, mam);
}
static TargetMachine *hostMachine() {
InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
Triple triple(sys::getDefaultTargetTriple());
std::string err;
const Target *target = TargetRegistry::lookupTarget(triple, err);
if (!target) {
std::fprintf(stderr, "error: %s\n", err.c_str());
std::exit(1);
}
return target->createTargetMachine(triple, "generic", "", TargetOptions(), Reloc::PIC_);
}
static void emitObject(Module &mod, TargetMachine *tm, const std::string &path) {
std::error_code ec;
raw_fd_ostream out(path, ec, sys::fs::OF_None);
if (ec) {
std::fprintf(stderr, "error: can't write '%s': %s\n", path.c_str(), ec.message().c_str());
std::exit(1);
}
legacy::PassManager pm;
if (tm->addPassesToEmitFile(pm, out, nullptr, CodeGenFileType::ObjectFile)) {
std::fprintf(stderr, "error: this machine can't produce object files\n");
std::exit(1);
}
pm.run(mod);
}
// 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");
if (!p) return -1;
char buf[4096];
size_t n;
while ((n = std::fread(buf, 1, sizeof buf, p)) > 0) output.append(buf, n);
int status = pclose(p);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
// pkg-config knows the right paths for a library if it has one; otherwise plain -l<name>.
static std::string libFlags(const std::string &lib) {
std::string out;
if (capture("pkg-config --libs '" + lib + "'", out) == 0) {
while (!out.empty() && std::isspace((unsigned char)out.back())) out.pop_back();
return " " + out;
}
return " '-l" + lib + "'";
}
// "GLFW/glfw3.h" -> "glfw": a decent first guess for the library's name.
static std::string guessLib(const std::string &header) {
std::string name = header.substr(header.find_last_of('/') + 1);
name = name.substr(0, name.rfind('.'));
while (!name.empty() && std::isdigit((unsigned char)name.back())) name.pop_back();
for (char &ch : name) ch = std::tolower((unsigned char)ch);
return name;
}
// The linker speaks in riddles; turn its complaints into something you can act on.
[[noreturn]] static void linkFailed(const std::string &output, const CImports &c) {
std::vector<std::string> missing, missingLibs;
auto addOnce = [](std::vector<std::string> &v, const std::string &x) {
if (std::find(v.begin(), v.end(), x) == v.end()) v.push_back(x);
};
std::istringstream lines(output);
for (std::string l; std::getline(lines, l);) {
size_t a;
if ((a = l.find("undefined reference to `")) != std::string::npos) {
a += 24;
addOnce(missing, l.substr(a, l.find('\'', a) - a));
} else if ((a = l.find("undefined symbol: ")) != std::string::npos) {
a += 18;
addOnce(missing, l.substr(a, l.find_first_of(" \n", a) - a));
} else if ((a = l.find("cannot find -l")) != std::string::npos) {
a += 14;
addOnce(missingLibs, l.substr(a, l.find_first_of(": \n", a) - a));
}
}
if (missing.empty() && missingLibs.empty()) {
std::fprintf(stderr, "%serror: linking failed\n", output.c_str());
std::exit(1);
}
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<std::pair<std::string, std::vector<std::string>>> byHeader;
for (const std::string &name : missing) {
auto f = c.fns.find(name);
std::string header = f == c.fns.end() ? "" : f->second.header;
auto it = std::find_if(byHeader.begin(), byHeader.end(), [&](auto &g) { return g.first == header; });
if (it == byHeader.end()) byHeader.push_back({header, {name}});
else it->second.push_back(name);
}
for (auto &[header, names] : byHeader) {
std::string list;
for (size_t k = 0; k < names.size() && k < 3; k++) list += (k ? ", " : "") + names[k];
if (names.size() > 3) list += " and " + std::to_string(names.size() - 3) + " more";
if (header.empty()) {
std::fprintf(stderr, "error: no code found for %s\n", list.c_str());
continue;
}
std::fprintf(stderr,
"error: %s come%s from \"%s\", but its library isn't linked\n"
" the header only says the functions exist; their code lives in a library.\n"
" add this at the top of your file (with the library's real name):\n\n"
" link \"%s\"\n\n",
list.c_str(), names.size() == 1 ? "s" : "", header.c_str(), guessLib(header).c_str());
}
std::exit(1);
}
static void link(const std::string &obj, const std::string &exe, const std::vector<std::string> &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 output;
int status = capture(cmd, output);
std::remove(obj.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);
}
if (status != 0) linkFailed(output, c);
std::fputs(output.c_str(), stderr); // warnings, if any
}
static std::string stem(const std::string &path) {
size_t slash = path.find_last_of('/');
std::string name = slash == std::string::npos ? path : path.substr(slash + 1);
size_t dot = name.rfind('.');
return dot == std::string::npos ? name : name.substr(0, dot);
}
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);
return 0;
}
if (argc < 3) usage();
std::string cmd = argv[1], file = argv[2];
std::string out = stem(file);
std::vector<std::string> libs;
bool optimizeCode = true;
for (int i = 3; i < argc; i++) {
std::string a = argv[i];
if (a == "-o" && i + 1 < argc) out = argv[++i];
else if (a == "-O0") optimizeCode = false;
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 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));
LLVMContext ctx;
TargetMachine *tm = hostMachine();
std::unique_ptr<Module> mod = generate(prog, c, ctx, *tm);
if (optimizeCode) optimize(*mod, tm);
if (cmd == "ir") {
mod->print(outs(), nullptr);
return 0;
}
std::string obj = out + ".o";
if (cmd == "run") {
SmallString<128> tmp;
sys::fs::createTemporaryFile("finch", "", tmp);
out = tmp.str().str();
obj = out + ".o";
}
emitObject(*mod, tm, obj);
link(obj, out, libs, c);
if (cmd == "run") {
int status = std::system(("'" + out + "'").c_str());
std::remove(out.c_str());
if (WIFSIGNALED(status)) {
std::fprintf(stderr, "\nthe program crashed: %s\n", strsignal(WTERMSIG(status)));
return 128 + WTERMSIG(status);
}
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}
return 0;
}

425
src/parser.cpp Normal file
View file

@ -0,0 +1,425 @@
#include "parser.h"
#include "error.h"
#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <unordered_map>
bool typeFromName(const std::string &name, Type &out) {
static const std::unordered_map<std::string, Type::Kind> names = {
{"int", Type::I64}, {"float", Type::F64}, {"bool", Type::Bool}, {"char", Type::Char},
{"str", Type::Str}, {"ptr", Type::Ptr}, {"i8", Type::I8}, {"i16", Type::I16},
{"i32", Type::I32}, {"i64", Type::I64}, {"u8", Type::U8}, {"u16", Type::U16},
{"u32", Type::U32}, {"u64", Type::U64}, {"f32", Type::F32}, {"f64", Type::F64},
};
auto it = names.find(name);
if (it == names.end()) return false;
out = Type(it->second);
return true;
}
namespace {
bool isTypeName(const std::string &name) {
Type t;
return typeFromName(name, t);
}
class Parser {
public:
explicit Parser(const std::vector<Token> &t) : toks(t) {}
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());
}
return p;
}
private:
const std::vector<Token> &toks;
size_t i = 0;
int parenDepth = 0; // inside ( ) 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}; }
std::string describe(const Token &t) const {
return t.kind == Tok::Ident ? "'" + t.text + "'" : tokName(t.kind);
}
bool accept(Tok k) {
if (!at(k)) return false;
i++;
return true;
}
const Token &expect(Tok k, const char *what = nullptr) {
if (!at(k))
fail(cur().line, cur().col,
std::string("expected ") + (what ? what : tokName(k)) + ", found " + describe(cur()));
return next();
}
[[noreturn]] void unexpected(const char *ctx) {
fail(cur().line, cur().col, "unexpected " + describe(cur()) + " " + ctx);
}
// A token can continue an expression only if it is on the same line (or we're inside parens).
bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; }
// int, u8, ptr, ptr[int], ptr[ptr[u8]] ...
Type type() {
if (!at(Tok::Ident) || !isTypeName(cur().text))
unexpected("(expected a type like int, float, u8, str or ptr[int])");
Type t;
typeFromName(next().text, t);
if (t.kind == Type::Ptr && accept(Tok::LBracket)) {
t = Type::ptrTo(type());
expect(Tok::RBracket);
}
return t;
}
// Does a declaration like `int x` or `ptr[int] p` start here?
bool atDeclaration() const {
if (!at(Tok::Ident) || !isTypeName(cur().text)) return false;
Tok n = peekTok().kind;
return n == Tok::Ident || (cur().text == "ptr" && n == Tok::LBracket);
}
// ---------- top level ----------
Import import() {
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;
return im;
}
Link link() {
Link l;
l.pos = pos();
expect(Tok::Link);
l.lib = expect(Tok::String, "a library name in quotes, like link \"glfw\"").text;
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;
}
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)");
fn.name = expect(Tok::Ident, "a function name").text;
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do {
Param p;
p.pos = pos();
p.type = type();
p.name = expect(Tok::Ident, "a parameter name").text;
fn.params.push_back(p);
} while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
fn.ret = accept(Tok::Arrow) ? type() : Type(Type::Void);
fn.body = block();
return fn;
}
// ---------- statements ----------
BlockPtr block() {
auto b = std::make_unique<BlockStmt>(pos());
expect(Tok::LBrace);
int saved = parenDepth;
parenDepth = 0;
while (!at(Tok::RBrace)) {
if (at(Tok::End)) fail(b->pos.line, b->pos.col, "this '{' is never closed (missing '}')");
b->body.push_back(statement());
endOfStatement();
}
parenDepth = saved;
next();
return b;
}
void endOfStatement() {
if (at(Tok::RBrace) || at(Tok::End) || cur().newlineBefore) return;
unexpected("(put each statement on its own line)");
}
StmtPtr statement() {
Pos p = pos();
switch (cur().kind) {
case Tok::If: return ifStmt();
case Tok::While: {
next();
auto s = std::make_unique<WhileStmt>(p);
s->cond = expr();
s->body = block();
return s;
}
case Tok::For: {
next();
auto s = std::make_unique<ForStmt>(p);
s->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();
s->body = block();
return s;
}
case Tok::Return: {
next();
auto s = std::make_unique<ReturnStmt>(p);
if (!at(Tok::RBrace) && !cur().newlineBefore) s->value = expr();
return s;
}
case Tok::Break: next(); return std::make_unique<BreakStmt>(p);
case Tok::Continue: next(); return std::make_unique<ContinueStmt>(p);
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::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
if (atDeclaration()) {
auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = true;
s->type = type();
s->name = next().text;
if (accept(Tok::Assign)) s->init = expr();
return s;
}
// x := 5
if (at(Tok::Ident) && peekTok().kind == Tok::Declare) {
auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = false;
s->name = next().text;
next();
s->init = expr();
return s;
}
ExprPtr e = expr();
// x = 5, p.value += 1, ...
char op = 0;
switch (cur().kind) {
case Tok::Assign: op = '='; break;
case Tok::PlusEq: op = '+'; break;
case Tok::MinusEq: op = '-'; break;
case Tok::StarEq: op = '*'; break;
case Tok::SlashEq: op = '/'; break;
case Tok::PercentEq: op = '%'; break;
case Tok::Declare:
fail(cur().line, cur().col, "':=' creates a new variable, so the left side must be a plain name");
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");
auto s = std::make_unique<AssignStmt>(p);
s->target = std::move(e);
s->op = op;
next();
s->value = expr();
return s;
}
if (e->kind != ExprKind::Call) fail(p.line, p.col, "this value is computed but never used");
return std::make_unique<ExprStmt>(p, std::move(e));
}
StmtPtr ifStmt() {
auto s = std::make_unique<IfStmt>(pos());
expect(Tok::If);
s->cond = expr();
s->then = block();
if (accept(Tok::Else)) {
if (at(Tok::If)) s->otherwise = ifStmt();
else s->otherwise = block();
}
return s;
}
// ---------- expressions (lowest to highest precedence) ----------
ExprPtr expr() { return orExpr(); }
ExprPtr orExpr() {
ExprPtr l = andExpr();
while (sameLine() && at(Tok::Or)) {
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, BinOp::Or, std::move(l), andExpr());
}
return l;
}
ExprPtr andExpr() {
ExprPtr l = compare();
while (sameLine() && at(Tok::And)) {
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, BinOp::And, std::move(l), compare());
}
return l;
}
ExprPtr compare() {
ExprPtr l = additive();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Eq: op = BinOp::Eq; break;
case Tok::NotEq: op = BinOp::NotEq; break;
case Tok::Less: op = BinOp::Less; break;
case Tok::LessEq: op = BinOp::LessEq; break;
case Tok::Greater: op = BinOp::Greater; break;
case Tok::GreaterEq: op = BinOp::GreaterEq; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), additive());
}
return l;
}
// Precedence is Go's, not C's: bit operators bind tighter than comparisons,
// so `x & 1 == 0` means `(x & 1) == 0`.
ExprPtr additive() { // + - | ^
ExprPtr l = multiplicative();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Plus: op = BinOp::Add; break;
case Tok::Minus: op = BinOp::Sub; break;
case Tok::Pipe: op = BinOp::BitOr; break;
case Tok::Caret: op = BinOp::BitXor; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), multiplicative());
}
return l;
}
ExprPtr multiplicative() { // * / % << >> &
ExprPtr l = unary();
while (sameLine()) {
BinOp op;
switch (cur().kind) {
case Tok::Star: op = BinOp::Mul; break;
case Tok::Slash: op = BinOp::Div; break;
case Tok::Percent: op = BinOp::Mod; break;
case Tok::Shl: op = BinOp::Shl; break;
case Tok::Shr: op = BinOp::Shr; break;
case Tok::Amp: op = BinOp::BitAnd; break;
default: return l;
}
Pos p = pos();
next();
l = std::make_unique<BinaryExpr>(p, op, std::move(l), unary());
}
return l;
}
ExprPtr unary() {
Pos p = pos();
if (accept(Tok::Minus)) return std::make_unique<UnaryExpr>(p, '-', unary());
if (accept(Tok::Not)) return std::make_unique<UnaryExpr>(p, '!', unary());
if (accept(Tok::Tilde)) return std::make_unique<UnaryExpr>(p, '~', unary());
return postfix();
}
// p.value
ExprPtr postfix() {
ExprPtr e = primary();
while (sameLine() && at(Tok::Dot)) {
Pos p = pos();
next();
e = std::make_unique<MemberExpr>(p, std::move(e), expect(Tok::Ident, "a name after '.'").text);
}
return e;
}
// name(arg, arg, ...) -- we are at the '('
ExprPtr callArgs(Pos p, const std::string &name) {
auto call = std::make_unique<CallExpr>(p, name);
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do call->args.push_back(expr());
while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
return call;
}
ExprPtr primary() {
Pos p = pos();
const Token &t = cur();
switch (t.kind) {
case Tok::Int: {
next();
errno = 0;
unsigned long long v = std::strtoull(t.text.c_str(), nullptr, t.text.size() > 1 && (t.text[1] == 'x' || t.text[1] == 'X') ? 16 : 10);
if (errno == ERANGE || v > (unsigned long long)LLONG_MAX)
fail(p.line, p.col, "number is too big for int");
return std::make_unique<IntExpr>(p, (long long)v);
}
case Tok::Float: next(); return std::make_unique<FloatExpr>(p, std::strtod(t.text.c_str(), nullptr));
case Tok::True: next(); return std::make_unique<BoolExpr>(p, true);
case Tok::False: next(); return std::make_unique<BoolExpr>(p, false);
case Tok::Null: next(); return std::make_unique<NullExpr>(p);
case Tok::Char: next(); return std::make_unique<CharExpr>(p, t.text[0]);
case Tok::String: next(); return std::make_unique<StrExpr>(p, t.text);
case Tok::LParen: {
next();
parenDepth++;
ExprPtr e = expr();
parenDepth--;
expect(Tok::RParen);
return e;
}
case Tok::Ident: {
next();
if (at(Tok::LParen) && !cur().newlineBefore) return callArgs(p, t.text);
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<VarExpr>(p, t.text);
}
default: unexpected("(expected a value)");
}
}
};
} // namespace
Program parse(const std::vector<Token> &tokens) {
return Parser(tokens).program();
}

6
src/parser.h Normal file
View file

@ -0,0 +1,6 @@
#pragma once
#include "ast.h"
#include "lexer.h"
Program parse(const std::vector<Token> &tokens);