mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 08:54:25 +00:00
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:
commit
0591cd20f3
73 changed files with 8066 additions and 0 deletions
235
src/ast.h
Normal file
235
src/ast.h
Normal 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
246
src/cimport.cpp
Normal 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
37
src/cimport.h
Normal 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
887
src/codegen.cpp
Normal 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
14
src/codegen.h
Normal 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
31
src/error.h
Normal 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
211
src/lexer.cpp
Normal 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
34
src/lexer.h
Normal 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
258
src/main.cpp
Normal 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
425
src/parser.cpp
Normal 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
6
src/parser.h
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "ast.h"
|
||||
#include "lexer.h"
|
||||
|
||||
Program parse(const std::vector<Token> &tokens);
|
||||
Loading…
Reference in a new issue