The compiler is written in Finch, and nothing else: src/*.fch, built from a seed

boot/ is src/ now. The C++ compiler and CMake are gone from this branch (they are on before-bootstrap).
bootstrap/build.sh builds the compiler from bootstrap/<system>.ll -- the compiler's own LLVM IR -- then builds
it with itself and checks the two come out identical. bootstrap/build.sh seed writes the seed anew.

The driver is complete: the runtime is compiled once and kept in ~/.cache/finch; a program's own C files,
.o and .a files and frameworks link along; a linker failure is read and answered with  link "lib"  or the
library that is missing. The old tests/fail diagnostics are all in again (a missing import, a str compared with
null, 'write just the library's name', a map key that is a float, non-ASCII names, ...).

tests/run.sh is the one test runner: programs, refusals, -g builds and the shared library. 106 passed.
This commit is contained in:
Nexoniarz 2026-10-25 21:42:17 +01:00
commit b9378050ab
39 changed files with 80403 additions and 7251 deletions

View file

@ -1,91 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(finch VERSION 2.4.0 LANGUAGES C CXX) # C: LLVM's CMake config checks C headers on some systems
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Using LLVM ${LLVM_PACKAGE_VERSION}")
add_executable(finch
src/main.cpp
src/lexer.cpp
src/parser.cpp
src/codegen.cpp
src/codegen_types.cpp
src/codegen_builtins.cpp
src/codegen_map.cpp
src/abi.cpp
src/cimport.cpp
src/target.cpp
src/loader.cpp
src/lsp.cpp
)
# The runtime (runtime/finch_rt.c) is embedded into the compiler as a string;
# finch compiles it once into ~/.cache/finch and links it into every program.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c)
# (written as several raw string pieces: MSVC limits one literal to 16 KB)
file(READ ${CMAKE_SOURCE_DIR}/runtime/finch_rt.c FINCH_RT_SOURCE)
string(LENGTH "${FINCH_RT_SOURCE}" FINCH_RT_LENGTH)
set(FINCH_RT_INC "static const char *kRuntimeSource =\n")
set(FINCH_RT_POS 0)
while(FINCH_RT_POS LESS FINCH_RT_LENGTH)
string(SUBSTRING "${FINCH_RT_SOURCE}" ${FINCH_RT_POS} 8000 FINCH_RT_PIECE)
string(APPEND FINCH_RT_INC "R\"FINCHRT(${FINCH_RT_PIECE})FINCHRT\"\n")
math(EXPR FINCH_RT_POS "${FINCH_RT_POS} + 8000")
endwhile()
string(APPEND FINCH_RT_INC ";\n")
file(WRITE ${CMAKE_BINARY_DIR}/rt_source.inc "${FINCH_RT_INC}")
target_include_directories(finch PRIVATE ${CMAKE_BINARY_DIR})
# libclang reads C headers for `import "stdio.h"`
find_path(LIBCLANG_INCLUDE clang-c/Index.h HINTS ${LLVM_INCLUDE_DIRS})
find_library(LIBCLANG NAMES clang libclang HINTS ${LLVM_LIBRARY_DIRS})
if(NOT LIBCLANG_INCLUDE OR NOT LIBCLANG)
message(FATAL_ERROR "libclang not found (on Nix: run inside nix-shell)")
endif()
target_include_directories(finch SYSTEM PRIVATE ${LLVM_INCLUDE_DIRS} ${LIBCLANG_INCLUDE})
target_compile_definitions(finch PRIVATE ${LLVM_DEFINITIONS} FINCH_VERSION="${PROJECT_VERSION}")
if(MSVC)
target_compile_options(finch PRIVATE /W3 /utf-8 /EHsc /wd4244 /wd4267 /wd4146 /wd4624)
else()
target_compile_options(finch PRIVATE -Wall -Wextra)
endif()
# The official Windows LLVM package names the DIA SDK library at the path of the machine it was
# built on (Visual Studio 2019); point it at the Visual Studio installed here.
if(MSVC AND TARGET LLVMDebugInfoPDB AND DEFINED ENV{VSINSTALLDIR})
get_target_property(FINCH_PDB_LIBS LLVMDebugInfoPDB INTERFACE_LINK_LIBRARIES)
if(FINCH_PDB_LIBS)
file(TO_CMAKE_PATH "$ENV{VSINSTALLDIR}DIA SDK/lib/amd64/diaguids.lib" FINCH_DIA_LIB)
list(TRANSFORM FINCH_PDB_LIBS REPLACE ".*diaguids\\.lib$" "${FINCH_DIA_LIB}")
set_target_properties(LLVMDebugInfoPDB PROPERTIES INTERFACE_LINK_LIBRARIES "${FINCH_PDB_LIBS}")
endif()
endif()
# Link the one shared libLLVM when available: libclang uses it too, and two copies
# of LLVM in one process crash on exit.
# Finch generates code for x86-64 and ARM64; use whichever of them this LLVM was built with.
set(FINCH_BACKENDS)
foreach(t X86 AArch64)
if(t IN_LIST LLVM_TARGETS_TO_BUILD)
list(APPEND FINCH_BACKENDS ${t})
string(TOUPPER ${t} T)
target_compile_definitions(finch PRIVATE FINCH_TARGET_${T})
endif()
endforeach()
message(STATUS "Finch backends: ${FINCH_BACKENDS}")
if(TARGET LLVM)
set(LLVM_LIBS LLVM)
else()
set(FINCH_COMPONENTS core support passes native)
foreach(t ${FINCH_BACKENDS})
list(APPEND FINCH_COMPONENTS ${t}codegen ${t}asmparser ${t}desc ${t}info)
endforeach()
llvm_map_components_to_libnames(LLVM_LIBS ${FINCH_COMPONENTS})
endif()
target_link_libraries(finch PRIVATE ${LLVM_LIBS} ${LIBCLANG})

49
bootstrap/build.sh Executable file
View file

@ -0,0 +1,49 @@
#!/bin/sh
# Builds the Finch compiler without a compiler for Finch: from the seed (the compiler's own LLVM IR),
# then from itself, and checks that the two come out the same.
#
# bootstrap/build.sh build/finch
# bootstrap/build.sh seed also write bootstrap/<system>.ll anew (do this when the compiler got a feature
# its own source uses, or the seed is old: the next build starts from it)
#
# Needs clang and libclang (the C headers are read through it). The compiler is then in build/finch; it looks for the
# runtime and the standard library next to build/, in runtime/ and std/.
set -eu
cd "$(dirname "$0")/.."
mkdir -p build
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) seed=linux-x86_64 ;;
Linux-aarch64|Linux-arm64) seed=linux-aarch64 ;;
Darwin-arm64) seed=macos-arm64 ;;
Darwin-x86_64) seed=macos-x86_64 ;;
MINGW*|MSYS*|CYGWIN*) seed=windows-x86_64 ;;
*) echo "bootstrap: no seed for $(uname -s) $(uname -m) (see bootstrap/README.md)" >&2; exit 1 ;;
esac
[ -f "bootstrap/$seed.ll" ] || { echo "bootstrap: bootstrap/$seed.ll is missing" >&2; exit 1; }
CLANG=${FINCH_CLANG:-clang}
LIBS=${FINCH_LIBCLANG:--lclang}
export FINCH_RT=$PWD/runtime/finch_rt.c
echo "bootstrap: seed ($seed) -> build/finch0"
$CLANG -O1 -w "bootstrap/$seed.ll" runtime/finch_rt.c -o build/finch0 -lm $LIBS
echo "bootstrap: build/finch0 builds the compiler -> build/finch1"
build/finch0 build src/main.fch -o build/finch1
echo "bootstrap: build/finch1 builds the compiler -> build/finch"
build/finch1 build src/main.fch -o build/finch
# a compiler that builds itself must come out the same however many times it does
build/finch1 ir src/main.fch > build/stage1.ll
build/finch ir src/main.fch > build/stage2.ll
if cmp -s build/stage1.ll build/stage2.ll; then
echo "bootstrap: build/finch1 and build/finch make the same IR ($(wc -l < build/stage2.ll) lines): done"
else
echo "bootstrap: the compiler does not reproduce itself (build/stage1.ll and build/stage2.ll differ)" >&2
exit 1
fi
if [ "${1:-}" = seed ]; then
cp build/stage2.ll "bootstrap/$seed.ll"
echo "bootstrap: wrote bootstrap/$seed.ll"
fi

79936
bootstrap/linux-x86_64.ll Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

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

356
src/ast.h
View file

@ -1,356 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
struct StructInfo; // filled in by codegen: fields, LLVM type, ownership
struct Type {
enum Kind {
Void, Bool, Char, I8, I16, I32, I64, U8, U16, U32, U64, F32, F64,
Str, Ptr, Null,
Array, // []T: growable, owns its elements
Map, // map[K]V: a hash map that keeps insertion order, owns its keys and values
Fixed, // [N]T: only inside C structs
Struct, // a resolved struct
Named, // a struct name as written, before codegen resolves it
};
Kind kind = Void;
std::shared_ptr<Type> elem; // Ptr (null = untyped `ptr`), Array, Fixed; Map: the value
std::shared_ptr<Type> key; // Map
long long count = 0; // Fixed
std::string name; // Named / Struct
std::string module; // Named: written qualifier ("" = none); Struct: defining module
StructInfo *info = nullptr; // Struct
Type() = default;
Type(Kind k) : kind(k) {}
static Type ptrTo(const Type &t) {
Type p(Ptr);
p.elem = std::make_shared<Type>(t);
return p;
}
static Type arrayOf(const Type &t) {
Type a(Array);
a.elem = std::make_shared<Type>(t);
return a;
}
static Type mapOf(const Type &k, const Type &v) {
Type m(Map);
m.key = std::make_shared<Type>(k);
m.elem = std::make_shared<Type>(v);
return m;
}
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;
switch (kind) {
case Ptr:
if (!elem || !o.elem) return !elem && !o.elem;
return *elem == *o.elem;
case Array: return *elem == *o.elem;
case Map: return *key == *o.key && *elem == *o.elem;
case Fixed: return count == o.count && *elem == *o.elem;
case Struct: return info == o.info;
case Named: return name == o.name && module == o.module;
default: return true;
}
}
bool operator!=(const Type &o) const { return !(*this == o); }
// How the type is written in Finch, for error messages.
std::string show() 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->show() + "]" : "ptr";
case Null: return "null";
case Array: return "[]" + elem->show();
case Map: return "map[" + key->show() + "]" + elem->show();
case Fixed: return "[" + std::to_string(count) + "]" + elem->show();
case Struct:
case Named: return module.empty() ? name : module + "." + name;
}
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;
int file = 0; // index into the list of source files (see error.h)
};
// ---------- expressions ----------
enum class ExprKind { Int, Float, Bool, Char, Str, Null, Var, Unary, Binary, Call, Member, Index, ArrayLit, Method,
MapLit, Try, OrElse };
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)) {}
};
// name(args): a function, a built-in, a conversion or a struct constructor.
struct CallExpr : Expr {
std::string callee;
std::vector<ExprPtr> args;
std::vector<std::string> argNames; // "" for positional; Point(x: 1, y: 2)
CallExpr(Pos p, std::string c) : Expr(ExprKind::Call, p), callee(std::move(c)) {}
};
struct MemberExpr : Expr { // obj.field, p.value, a.len
ExprPtr obj;
std::string field;
MemberExpr(Pos p, ExprPtr o, std::string f)
: Expr(ExprKind::Member, p), obj(std::move(o)), field(std::move(f)) {}
};
struct IndexExpr : Expr { // a[i]
ExprPtr obj, index;
IndexExpr(Pos p, ExprPtr o, ExprPtr i) : Expr(ExprKind::Index, p), obj(std::move(o)), index(std::move(i)) {}
};
struct ArrayLitExpr : Expr { // [1, 2, 3]
std::vector<ExprPtr> elems;
explicit ArrayLitExpr(Pos p) : Expr(ExprKind::ArrayLit, p) {}
};
struct MapLitExpr : Expr { // ["a": 1, "b": 2], or [:] for an empty map
std::vector<ExprPtr> keys, values;
explicit MapLitExpr(Pos p) : Expr(ExprKind::MapLit, p) {}
};
// obj.name(args): a.push(x), s.find("x"), p.move(1, 2), and math.sqrt(2) / math.Vec(1, 2) for modules
struct MethodExpr : Expr {
ExprPtr obj;
std::string name;
std::vector<ExprPtr> args;
std::vector<std::string> argNames;
MethodExpr(Pos p, ExprPtr o, std::string n) : Expr(ExprKind::Method, p), obj(std::move(o)), name(std::move(n)) {}
};
struct BlockStmt;
// try f(x): if f fails, the current function fails with the same error
struct TryExpr : Expr {
ExprPtr call;
TryExpr(Pos p, ExprPtr c) : Expr(ExprKind::Try, p), call(std::move(c)) {}
};
// f(x) or fallback, f(x) or { ...err... }
struct OrElseExpr : Expr {
ExprPtr call;
ExprPtr fallback; // or a value
std::unique_ptr<BlockStmt> block; // or a block that sees `err`
OrElseExpr(Pos p, ExprPtr c) : Expr(ExprKind::OrElse, p), call(std::move(c)) {}
~OrElseExpr() override;
};
// ---------- statements ----------
enum class StmtKind { VarDecl, Assign, Expr, If, While, For, ForEach, Return, Break, Continue, Block, Defer };
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;
Pos end; // the closing '}'
explicit BlockStmt(Pos p) : Stmt(StmtKind::Block, p) {}
};
using BlockPtr = std::unique_ptr<BlockStmt>;
inline OrElseExpr::~OrElseExpr() = default;
struct VarDeclStmt : Stmt {
bool hasType; // `int x = 5` vs `x := 5`
Type type;
std::string name;
Pos namePos;
ExprPtr init; // may be null only when hasType
VarDeclStmt(Pos p) : Stmt(StmtKind::VarDecl, p) {}
};
struct AssignStmt : Stmt {
ExprPtr target; // a variable, field, element 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;
Pos varPos;
ExprPtr from, to; // for var in from..to (to is exclusive)
BlockPtr body;
ForStmt(Pos p) : Stmt(StmtKind::For, p) {}
};
struct ForEachStmt : Stmt {
std::string var;
Pos varPos;
std::string var2; // for i, x in list / for k, v in map ("" = one variable)
Pos var2Pos;
ExprPtr list; // for var in list (an array, a str or a map)
BlockPtr body;
ForEachStmt(Pos p) : Stmt(StmtKind::ForEach, 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) {}
};
struct DeferStmt : Stmt { // defer <statement>: runs when the block ends
StmtPtr body;
DeferStmt(Pos p, StmtPtr s) : Stmt(StmtKind::Defer, p), body(std::move(s)) {}
};
// ---------- top level ----------
struct Param {
Type type;
std::string name;
Pos pos; // the type
Pos namePos; // the name
};
struct FnDecl {
Pos pos;
Pos namePos;
std::string name;
std::string recv; // fn Point.move(...): "Point" (a method; `self` is the Point it is called on)
Pos recvPos;
Type ret; // Void when there is no `-> type`
bool fallible = false; // -> int! / -> ! : can fail with an error
std::vector<Param> params;
BlockPtr body;
};
struct Field {
Type type;
std::string name;
ExprPtr init; // default value, may be null
Pos pos;
Pos namePos;
};
struct StructDecl {
Pos pos;
Pos namePos;
std::string name;
std::vector<Field> fields;
};
struct Import {
Pos pos;
std::string path; // "stdio.h" (C header, isC) or "math" (Finch module)
bool isC = true;
};
struct Link {
Pos pos;
std::string lib; // "glfw" -> libglfw.so; "helpers.c" / ".o" / ".a" -> a file next to the .fch file
};
// One .fch file.
struct Program {
std::string module; // "" for the main file, else the module's name
std::string path;
std::vector<Import> imports;
std::vector<Link> links;
std::vector<StructDecl> structs;
std::vector<FnDecl> fns;
};

View file

@ -1,80 +0,0 @@
#pragma once
// One description of every built-in function and method, shared by the language server
// (hover, completion, signature help).
struct BuiltinDoc {
const char *name;
const char *signature;
const char *doc;
};
inline const BuiltinDoc kBuiltins[] = {
{"print", "print(a, b, ...)", "Prints the values separated by spaces, then a new line. Works with every type."},
{"input", "input(str question = \"\") -> str", "Shows the question and reads one line typed by the user, without the newline."},
{"read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read, unless handled: read_file(p) or { ... }, try read_file(p)."},
{"write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked; with or / try, the reason it didn't."},
{"file_exists", "file_exists(str path) -> bool", "Is there a file at this path?"},
{"delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."},
{"error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."},
{"shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."},
{"exit", "exit(int code)", "Ends the program right away with this exit code."},
{"addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."},
{"new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."},
{"free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."},
{"str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."},
{"int", "int(x) -> int", "Converts to a whole number: cuts off fractions, parses text (stops the program if it isn't a number, unless handled: int(s) or 0)."},
{"float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."},
{"char", "char(int n) -> char", "The character with this code."},
{"bool", "bool(int n) -> bool", "true for any number but 0."},
{"ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C)."},
};
inline const BuiltinDoc kArrayMethods[] = {
{"len", ".len -> int", "The number of elements."},
{"ptr", ".ptr -> ptr[T]", "The address of the first element, for C."},
{"push", "push(T x)", "Adds x at the end."},
{"pop", "pop() -> T", "Removes the last element and gives it back."},
{"insert", "insert(int i, T x)", "Puts x at position i, moving the rest."},
{"remove", "remove(int i) -> T", "Removes position i and gives it back."},
{"clear", "clear()", "Removes everything."},
{"resize", "resize(int n)", "Makes the array n long; new elements are zero."},
{"find", "find(T x) -> int", "The index of x, or -1."},
{"contains", "contains(T x) -> bool", "Is x in the array?"},
{"sort", "sort()", "Sorts in place (numbers, chars, str)."},
{"reverse", "reverse()", "Reverses in place."},
{"slice", "slice(int start, int end) -> []T", "A new array with the elements start to end-1."},
{"join", "join(str sep) -> str", "For []str: one text with sep between the parts."},
};
inline const BuiltinDoc kStrMethods[] = {
{"len", ".len -> int", "The number of bytes."},
{"ptr", ".ptr -> ptr[char]", "The address of the text, for C (valid while the str lives)."},
{"sub", "sub(int start, int end) -> str", "The part from start to end-1."},
{"find", "find(str t) -> int", "Where t starts, or -1."},
{"contains", "contains(str t) -> bool", "Does the text contain t?"},
{"starts_with", "starts_with(str t) -> bool", "Does the text start with t?"},
{"ends_with", "ends_with(str t) -> bool", "Does the text end with t?"},
{"split", "split(str sep) -> []str", "The parts between each sep; split(\"\") gives single characters."},
{"trim", "trim() -> str", "Without spaces and newlines at both ends."},
{"upper", "upper() -> str", "With a-z changed to A-Z."},
{"lower", "lower() -> str", "With A-Z changed to a-z."},
{"replace", "replace(str from, str to) -> str", "Every from replaced by to."},
{"repeat", "repeat(int n) -> str", "The text n times."},
{"bytes", "bytes() -> []u8", "The bytes of the text."},
};
inline const BuiltinDoc kMapMethods[] = {
{"len", ".len -> int", "The number of keys."},
{"has", "has(K key) -> bool", "Is the key in the map?"},
{"get", "get(K key, V default) -> V", "The value for key, or default if the key isn't there."},
{"remove", "remove(K key) -> bool", "Removes the key and its value; true if it was there."},
{"clear", "clear()", "Removes everything."},
{"keys", "keys() -> []K", "The keys, in the order they were added."},
{"values", "values() -> []V", "The values, in the order their keys were added."},
};
inline const char *kKeywords[] = {"fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true",
"false", "null", "import", "link", "struct", "defer", "or", "try", "self"};
inline const char *kTypeNames[] = {"int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64",
"u8", "u16", "u32", "u64", "f32", "f64"};

View file

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

View file

@ -1,56 +0,0 @@
#pragma once
#include "ast.h"
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
// What Finch learned from `import "something.h"`.
// C structs by value appear in types as Named{name, module "C"}.
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 CField {
std::string name;
Type type;
long long offset = 0; // bytes
long long size = 0, align = 1;
bool hidden = false; // unions, bitfields, unmappable types: kept for layout only
};
struct CStruct {
std::string name;
std::vector<CField> fields;
long long size = 0, align = 1;
std::string unsupported; // why it can't be used by value (pointers to it still work)
bool hasHidden = false;
};
struct CImports {
std::unordered_map<std::string, CFunc> fns;
std::unordered_map<std::string, CConst> consts;
std::unordered_map<std::string, CGlobal> globals;
std::map<std::string, CStruct> structs;
};
// `dirs`: folders searched first for "local.h" headers (the .fch files' folders).
CImports importHeaders(const std::vector<Import> &imports, const std::vector<std::string> &dirs);

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1,327 +0,0 @@
#pragma once
// Internal to the code generator: the Codegen class, shared by codegen*.cpp and abi.cpp.
#include "codegen.h"
#include "error.h"
#include "index.h"
#include <llvm/IR/Constants.h>
#include <llvm/IR/DIBuilder.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/Verifier.h>
#include <llvm/Support/raw_ostream.h>
#include <functional>
#include <map>
#include <set>
#include <unordered_map>
using FType = ::Type; // Finch types (llvm::Type is the LLVM one)
struct StructInfo {
std::string name;
std::string module; // "" main file, a module name, or "C" for imported C structs
bool isC = false;
const StructDecl *decl = nullptr; // Finch structs
const CStruct *cstruct = nullptr; // C structs
struct F {
std::string name;
FType type;
const Expr *init = nullptr; // default value
bool hidden = false; // C union/bitfield padding: has no name in Finch
unsigned llvmIndex = 0; // C structs carry explicit padding between fields
Pos pos; // Finch structs: where the field is declared
};
std::vector<F> fields;
llvm::StructType *llvm = nullptr;
int state = 0; // 0 = not resolved, 1 = resolving (cycle check), 2 = done
bool owning = false;
std::string unsupported; // C structs Finch can't lay out
Pos pos;
};
namespace finch {
struct Value_ {
llvm::Value *v;
FType type;
bool literal = false; // a number written in the code (or a C constant): adapts to the type around it
bool fresh = false; // an owning value nobody holds yet: it must be stored (moved) or released
};
struct Var {
llvm::Value *slot;
FType type;
bool readonly = false; // loop variables
std::string readonlyWhy;
bool owned = false; // dropped when its scope ends
int order = 0; // declaration order, for defer
bool moved = false; // returned out of the function: not dropped on that path
Pos pos; // where it was declared
};
struct Cleanup {
bool isDefer;
std::string var; // Drop: the variable
const Stmt *body = nullptr; // Defer: the statement
int order = 0; // Defer: variables declared after it are not visible
};
struct Scope {
std::unordered_map<std::string, Var> vars;
std::vector<Cleanup> cleanups;
};
struct Fn {
llvm::Function *llvm = nullptr;
const FnDecl *decl = nullptr;
std::string module;
std::vector<bool> borrowParam; // owning params the function never changes: no copy
llvm::Function *cThunk = nullptr;
FType ret; // resolved types
std::vector<FType> params;
StructInfo *recv = nullptr; // a method of this struct: the first LLVM parameter is `self`'s address
bool mutatesSelf = false; // the method changes `self`
bool fallible = false; // -> T!: returns { i1 ok, T value, str error }
};
struct Loop {
llvm::BasicBlock *continueTo, *breakTo;
size_t scopeDepth; // scopes opened inside the loop are cleaned up by break/continue
size_t pendingDepth; // and so are temporaries held by expressions around it (see Codegen::pending)
};
// A fresh value an expression is holding while it computes another part (f(a + b, try g())): if that
// part leaves early (try, or a return/break/continue in an `or { }` block), it must be dropped.
struct Pending {
llvm::Value *v;
FType type;
bool isAddr; // v is the value's address
};
struct Place { // something that can be assigned to
llvm::Value *addr;
FType type;
};
struct LRef {
bool isPlace;
Place pl;
Value_ val;
};
struct ModuleScope {
std::map<std::string, Fn> fns;
std::map<std::string, StructInfo *> structs;
std::set<std::string> imports; // Finch modules visible here
};
// How a C struct travels: System V x86-64 (Linux) or Microsoft x64 (Windows).
struct AbiArg {
// Direct: as is. Expand: split into register-sized parts. Memory: on the stack (byval), or
// returned through a hidden pointer (sret). Indirect (Windows): a pointer to a copy the caller made.
enum Kind { Direct, Expand, Memory, Indirect } kind = Direct;
std::vector<llvm::Type *> parts; // Expand: one value per eightbyte
llvm::Type *coerced = nullptr; // Expand: { parts... } for returns
bool stackAlign8 = false; // AArch64 Linux: HFA arguments that end up on the stack
};
class Codegen {
public:
Codegen(llvm::LLVMContext &c, const std::vector<Program> &progs, const CImports &ci, llvm::TargetMachine &tm,
bool debug);
std::unique_ptr<llvm::Module> run();
// ---------- state ----------
llvm::LLVMContext &ctx;
llvm::IRBuilder<> b;
std::unique_ptr<llvm::Module> mod;
const std::vector<Program> &progs;
const CImports &cimports;
const llvm::DataLayout *dl;
std::map<std::string, ModuleScope> modules;
std::map<std::string, StructInfo *> cStructs;
std::vector<std::unique_ptr<StructInfo>> structStore;
std::vector<Scope> scopes;
std::vector<Loop> loops;
std::vector<Pending> pending;
const Fn *curFn = nullptr;
std::string curModule;
int varOrder = 0;
int deferLimit = -1; // while emitting a defer: hide variables declared after it
int inDefer = 0;
std::map<std::string, llvm::Function *> helpers; // drop/copy/print per type
std::map<StructInfo *, std::map<std::string, Fn>> methods;
std::set<std::string> mutatingMethods; // names of methods that change self (for the mutation analysis)
// errors as values: the call that `or` / `try` is handling reports here whether it failed
const Expr *fallibleTarget = nullptr;
llvm::Value *fallibleFailed = nullptr; // i1
llvm::Value *fallibleMsg = nullptr; // str
// debug info (-g)
bool debug;
std::unique_ptr<llvm::DIBuilder> di;
std::vector<llvm::DIFile *> diFiles;
llvm::DICompileUnit *diUnit = nullptr;
llvm::DISubprogram *diFn = nullptr;
std::map<std::string, llvm::DIType *> diTypes;
// ---------- codegen.cpp: program, statements, expressions ----------
void declareStructs();
void declareFns();
void declare(const Program &p, const FnDecl &fn);
void define(Fn &fn);
void defineMainWrapper();
void block(const BlockStmt &blk);
void blockBody(const BlockStmt &blk);
void stmt(const Stmt &s);
void varDecl(const VarDeclStmt &s);
void assign(const AssignStmt &s);
void ifStmt(const IfStmt &s);
void whileStmt(const WhileStmt &s);
void forStmt(const ForStmt &s);
void forEachStmt(const ForEachStmt &s);
void returnStmt(const ReturnStmt &s);
llvm::Type *retType(const Fn &f);
void retValue(llvm::Value *v); // return from the current function (wrapped as success if it can fail)
void retError(llvm::Value *msg); // fail the current function with the str `msg`
void jump(bool isBreak, Pos p);
llvm::Value *condition(const Expr &e);
void continueAt(llvm::BasicBlock *bb, llvm::BasicBlock *deadEnd);
bool terminated();
llvm::BasicBlock *newBlock(const char *name);
void hold(const Value_ &v); // see Pending
void holdAddr(llvm::Value *addr, const FType &t);
void dropPending(size_t downTo); // emit drops for pending[downTo..]
struct Held { // forgets what was held when the expression is done
Codegen &cg;
size_t mark;
explicit Held(Codegen &c) : cg(c), mark(c.pending.size()) {}
~Held() { cg.pending.erase(cg.pending.begin() + mark, cg.pending.end()); }
};
void pushScope();
void popScope(); // emits the scope's cleanups if reachable
void emitCleanups(size_t downTo); // run cleanups of scopes [downTo, end) without popping
void emitScopeCleanups(size_t idx);
llvm::AllocaInst *slot(const FType &t, const std::string &name);
Var *lookup(const std::string &name);
Var &addVar(Pos p, const std::string &name, const FType &t, llvm::Value *init, bool owned, bool readonly = false,
unsigned argNo = 0);
Value_ expr(const Expr &e);
Value_ exprWant(const Expr &e, const FType &want);
Value_ binary(const BinaryExpr &e);
Value_ unary(const UnaryExpr &e);
Value_ logic(const BinaryExpr &e);
Value_ arith(BinOp op, Value_ l, Value_ r, Pos p);
Value_ arithOp(BinOp op, Value_ l, Value_ r, Pos p);
Value_ compare(BinOp op, Value_ l, Value_ r, Pos p);
bool unify(Value_ &l, Value_ &r);
[[noreturn]] void badOperands(BinOp op, const Value_ &l, const Value_ &r, Pos p);
LRef ref(const Expr &e, bool forWrite = false); // an address if `e` has one, else a value
Place place(const Expr &e, bool forWrite = false);
LRef member(const MemberExpr &m, bool forWrite);
LRef index(const IndexExpr &e, bool forWrite);
Value_ arrayLit(const ArrayLitExpr &a, const FType *want);
Value_ fallible(const Expr &call, llvm::Value *&failed, llvm::Value *&msg);
Value_ tryExpr(const TryExpr &t);
Value_ orElse(const OrElseExpr &e, bool discard = false); // discard: a statement, the value isn't used
FType structType(StructInfo *s);
std::string rootVar(const Expr &e);
bool mutates(const std::string &name, const Stmt &s);
bool mutatesExpr(const std::string &name, const Expr &e);
// ---------- codegen_types.cpp: types, conversions, ownership, printing ----------
llvm::Type *ty(const FType &t);
FType resolve(const FType &t, Pos p);
FType resolveT(const FType &t, Pos p, bool deep); // deep: structs by value must be complete
void forN(llvm::Value *n, const std::function<void(llvm::Value *)> &body); // for i in 0..n
llvm::Value *tmpOf(llvm::Type *t); // an uninitialised stack slot
void resolveStruct(StructInfo *s);
StructInfo *findStruct(const std::string &module, const std::string &name, bool qualified);
bool owning(const FType &t);
llvm::Constant *zero(const FType &t);
Value_ defaultValue(const FType &t, Pos p);
static bool widens(const FType &from, const FType &to);
static bool fits(llvm::ConstantInt *c, const FType &from, const FType &to);
llvm::Value *intCast(llvm::Value *v, const FType &from, const FType &to);
llvm::Value *intToFloat(llvm::Value *v, const FType &from, const FType &to);
Value_ coerce(Value_ v, const FType &want, Pos p, const std::string &what);
llvm::Value *own(const Value_ &v); // something that can be stored: moved if fresh, else copied
void release(const Value_ &v); // drop a fresh value nobody kept
llvm::Value *copyValue(llvm::Value *v, const FType &t);
void dropValue(llvm::Value *v, const FType &t);
void dropAt(llvm::Value *addr, const FType &t);
void copyAt(llvm::Value *dst, llvm::Value *src, const FType &t);
llvm::Function *dropFn(const FType &t);
llvm::Function *copyFn(const FType &t);
std::string typeKey(const FType &t);
llvm::Value *tmp(llvm::Value *v); // a stack copy of v, to pass by address
void emitPrint(llvm::Value *v, const FType &t, bool quoted);
void emitFormat(llvm::Value *buf, llvm::Value *v, const FType &t, bool quoted); // append v as text to the str at buf
void bufText(llvm::Value *buf, const std::string &text);
llvm::Function *formatFn(const FType &t);
void printf_(const std::string &fmt, std::vector<llvm::Value *> args);
llvm::Value *strConst(const std::string &s);
llvm::Value *cstr(llvm::Value *strValue); // str -> char* for C
Value_ strFromC(llvm::Value *p); // char* -> borrowed str
llvm::Value *elemSize(const FType &t);
// ---------- codegen_builtins.cpp: calls, built-ins, methods ----------
Value_ call(const CallExpr &c);
Value_ method(const MethodExpr &m);
Value_ callFinch(const Fn &fn, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p,
const std::string &shownName, const Expr *site, llvm::Value *self = nullptr,
const std::string &selfRoot = "");
Value_ construct(StructInfo *s, const std::vector<ExprPtr> &args, const std::vector<std::string> &names, Pos p);
Value_ callC(const CFunc &f, const std::vector<ExprPtr> &args, Pos p);
Value_ convert(const FType &to, const std::vector<ExprPtr> &args, Pos p, const std::string &name, bool canFail = false);
Value_ print(const std::vector<ExprPtr> &args, Pos p);
Value_ addrOf(const std::vector<ExprPtr> &args, Pos p);
Value_ arrayMethod(const MethodExpr &m, Value_ *recv);
Value_ structMethod(const MethodExpr &m, LRef &r, const FType &t);
// ---------- codegen_map.cpp: maps ----------
llvm::StructType *mapEntryTy(const FType &t);
Value_ mapLit(const MapLitExpr &e, const FType *want);
LRef mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite);
Value_ mapMethod(const MethodExpr &m, Value_ *recv);
std::vector<llvm::Value *> mapArgs(llvm::Value *map, llvm::Value *keyAddr, const FType &t);
llvm::Value *mapSlot(llvm::Value *map, const FType &t, const Value_ &key, Pos p, bool fill);
llvm::Value *mapEntry(llvm::Value *map, const FType &t, llvm::Value *i);
void forEachEntry(llvm::Value *map, const FType &t, const std::function<void(llvm::Value *i, llvm::Value *entry)> &body);
llvm::Value *keyText(const Value_ &key);
bool isBuiltin(const std::string &n);
std::string headerHint(const std::string &name);
void checkArgs(const std::vector<ExprPtr> &args, const std::vector<std::string> &names, size_t want, Pos p,
const std::string &name, bool variadic = false);
llvm::Value *cGlobal(const std::string &name);
llvm::Function *rt(const std::string &name); // a runtime function by name
// runtime errors
llvm::Value *fileName(Pos p);
void panicIf(llvm::Value *cond, const std::string &msg, Pos p);
void boundsCheck(llvm::Value *i, llvm::Value *len, Pos p);
void checkDivisor(const Value_ &l, const Value_ &r, Pos p);
llvm::Function *libc(const char *name, llvm::Type *ret, std::vector<llvm::Type *> params, bool vararg = false);
// ---------- abi.cpp: C calling convention for structs by value ----------
AbiArg classify(const FType &t);
llvm::Function *declareC(const CFunc &f);
Value_ emitCCall(const CFunc &f, llvm::Function *fn, std::vector<Value_> args, Pos p);
llvm::Function *cThunk(Fn &fn, Pos p);
// ---------- language server index ----------
void note(Pos at, size_t len, const std::string &hover, Pos def = Pos{});
std::string signature(const Fn &fn);
std::string structHover(StructInfo *s);
// ---------- debug info ----------
void setLoc(Pos p);
llvm::DIType *diType(const FType &t);
};
} // namespace finch

View file

@ -1,246 +0,0 @@
// Code generation, part 4: maps.
//
// map[K]V is { entries, len, used, cap, index, icap } (see runtime/finch_rt.c). The runtime finds and
// places entries; the compiler stores, copies and drops the keys and values in them, like it does
// for array elements. An entry is { i64 hash, K key, V value }; hash 0 marks a removed entry.
#include "codegen_impl.h"
#include "builtins_doc.h"
using namespace llvm;
namespace finch {
StructType *Codegen::mapEntryTy(const FType &t) {
return StructType::get(ctx, {b.getInt64Ty(), ty(*t.key), ty(*t.elem)});
}
// The runtime's (map, key, entry size, key size, key is str) arguments.
std::vector<Value *> Codegen::mapArgs(Value *map, Value *keyAddr, const FType &t) {
return {map, keyAddr, b.getInt64(dl->getTypeAllocSize(mapEntryTy(t))), elemSize(*t.key),
b.getInt32(t.key->kind == FType::Str ? 1 : 0)};
}
Value *Codegen::mapEntry(Value *map, const FType &t, Value *i) {
Value *entries = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(t), map, 0));
return b.CreateInBoundsGEP(mapEntryTy(t), entries, i);
}
// The entry for `key`, added if it's missing: the key is copied in, and the value is its type's
// default when `fill` (m[k] += 1 starts from 0).
Value *Codegen::mapSlot(Value *map, const FType &t, const Value_ &key, Pos p, bool fill) {
StructType *ET = mapEntryTy(t);
Value *keyAddr = tmp(key.v);
Value *added = tmpOf(b.getInt32Ty());
std::vector<Value *> args = mapArgs(map, keyAddr, t);
args.push_back(added);
Value *entry = mapEntry(map, t, b.CreateCall(rt("finch_map_slot"), args));
BasicBlock *fresh = newBlock("map.new"), *have = newBlock("map.have");
b.CreateCondBr(b.CreateICmpNE(b.CreateLoad(b.getInt32Ty(), added), b.getInt32(0)), fresh, have);
b.SetInsertPoint(fresh);
copyAt(b.CreateStructGEP(ET, entry, 1), keyAddr, *t.key);
if (fill) b.CreateStore(own(defaultValue(*t.elem, p)), b.CreateStructGEP(ET, entry, 2));
b.CreateBr(have);
b.SetInsertPoint(have);
return entry;
}
// Every live entry, in insertion order: body(entry number, entry address).
void Codegen::forEachEntry(Value *map, const FType &t, const std::function<void(Value *, Value *)> &body) {
Value *used = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 2));
forN(used, [&](Value *i) {
Value *entry = mapEntry(map, t, i);
Value *hash = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(mapEntryTy(t), entry, 0));
BasicBlock *live = newBlock("entry.live"), *next = newBlock("entry.next");
b.CreateCondBr(b.CreateICmpNE(hash, b.getInt64(0)), live, next);
b.SetInsertPoint(live);
body(i, entry);
b.CreateBr(next);
b.SetInsertPoint(next);
});
}
// A key as text for error messages: "bob" or 42
Value *Codegen::keyText(const Value_ &key) {
Value *out = tmpOf(ty(FType::Str));
const FType &t = key.type;
if (t.kind == FType::Str) {
Value *q = tmp(strConst("\"")), *half = tmpOf(ty(FType::Str));
b.CreateCall(rt("finch_str_concat"), {half, q, tmp(key.v)});
b.CreateCall(rt("finch_str_concat"), {out, half, q});
} else if (t.isSigned()) {
b.CreateCall(rt("finch_str_from_int"), {out, b.CreateSExt(key.v, b.getInt64Ty())});
} else if (t.isUnsigned()) {
b.CreateCall(rt("finch_str_from_uint"), {out, b.CreateZExt(key.v, b.getInt64Ty())});
} else if (t.kind == FType::Char) {
b.CreateCall(rt("finch_str_from_char"), {out, key.v});
} else {
b.CreateCall(rt("finch_str_from_bool"), {out, b.CreateZExt(key.v, b.getInt32Ty())});
}
return out;
}
// m[key]: reading a missing key stops the program; writing (m[k] = v, m[k] += 1, m[k].push(x)) adds it
LRef Codegen::mapIndex(const IndexExpr &e, LRef &obj, const FType &t, bool forWrite) {
Pos p = e.pos;
FType kt = *t.key, vt = *t.elem;
Value_ key = coerce(exprWant(*e.index, kt), kt, e.index->pos, "the key");
if (forWrite) {
if (!obj.isPlace) failAt(p.file, p.line, p.col, "this is a temporary map, it can't be changed");
Value *entry = mapSlot(obj.pl.addr, t, key, p, true);
release(key);
return {true, {b.CreateStructGEP(mapEntryTy(t), entry, 2), vt}, {}};
}
Value *map = obj.isPlace ? obj.pl.addr : tmp(obj.val.v);
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(key.v), t));
BasicBlock *missing = newBlock("map.missing"), *found = newBlock("map.found");
b.CreateCondBr(b.CreateICmpSLT(i, b.getInt64(0)), missing, found);
b.SetInsertPoint(missing);
b.CreateCall(rt("finch_map_missing"), {fileName(p), b.getInt64(p.line), keyText(key)});
b.CreateUnreachable();
b.SetInsertPoint(found);
release(key);
Value *valueAddr = b.CreateStructGEP(mapEntryTy(t), mapEntry(map, t, i), 2);
if (obj.isPlace) return {true, {valueAddr, vt}, {}};
Value *v = b.CreateLoad(ty(vt), valueAddr); // from a temporary map: take the value, drop the rest
Value_ out{v, vt};
if (owning(vt) && obj.val.fresh) out = {copyValue(v, vt), vt, false, true};
release(obj.val);
return {false, {}, out};
}
// ["a": 1, "b": 2], or [:] where the type is known
Value_ Codegen::mapLit(const MapLitExpr &e, const FType *want) {
Pos p = e.pos;
FType t;
Value_ k0, v0; // the first entry, when it decides the types
bool first = false;
if (want && want->kind == FType::Map) {
t = *want;
} else if (e.keys.empty()) {
failAt(p.file, p.line, p.col, "an empty [:] needs a type, like: map[str]int m = [:]");
} else {
// number values written in the code are all floats if any of them has a dot
bool allNum = true, anyFloat = false;
for (auto &x : e.values) {
const Expr *y = x.get();
if (y->kind == ExprKind::Unary && static_cast<const UnaryExpr *>(y)->op == '-') y = static_cast<const UnaryExpr *>(y)->operand.get();
if (y->kind == ExprKind::Float) anyFloat = true;
else if (y->kind != ExprKind::Int) allNum = false;
}
k0 = expr(*e.keys[0]);
v0 = expr(*e.values[0]);
Pos kp = e.keys[0]->pos, vp = e.values[0]->pos;
if (k0.type.kind == FType::Void || k0.type.kind == FType::Null) failAt(kp.file, kp.line, kp.col, "a map key needs a value with a type");
if (v0.type.kind == FType::Void || v0.type.kind == FType::Null) failAt(vp.file, vp.line, vp.col, "a map value needs a value with a type");
t = resolve(FType::mapOf(k0.type, allNum && anyFloat ? FType(FType::F64) : v0.type), kp);
first = true;
}
Value *map = tmp(zero(t));
StructType *ET = mapEntryTy(t);
Held held(*this);
holdAddr(map, t); // the entries so far, if a later one leaves early
for (size_t i = 0; i < e.keys.size(); i++) {
Held entry(*this);
Value_ k = i == 0 && first ? k0 : exprWant(*e.keys[i], *t.key);
k = coerce(k, *t.key, e.keys[i]->pos, "every key of this map");
hold(k);
Value_ v = i == 0 && first ? v0 : exprWant(*e.values[i], *t.elem);
Value *vv = own(coerce(v, *t.elem, e.values[i]->pos, "every value of this map"));
Value *valueAddr = b.CreateStructGEP(ET, mapSlot(map, t, k, p, false), 2);
dropAt(valueAddr, *t.elem); // a key given twice: the last value wins
b.CreateStore(vv, valueAddr);
release(k);
}
return {b.CreateLoad(ty(t), map), t, false, true};
}
Value_ Codegen::mapMethod(const MethodExpr &m, Value_ *recv) {
Pos p = m.pos;
const std::string &n = m.name;
FType t = recv->type, kt = *t.key, vt = *t.elem;
StructType *ET = mapEntryTy(t);
Value *map = recv->v;
auto need = [&](size_t k) { checkArgs(m.args, m.argNames, k, p, n); };
auto keyArg = [&]() { return coerce(exprWant(*m.args[0], kt), kt, m.args[0]->pos, "the key"); };
if (g_index)
for (const BuiltinDoc &d : kMapMethods)
if (n == d.name) note(Pos{p.line, p.col + 1, p.file}, n.size(), "```finch\n" + std::string(d.signature) + "\n```\n" + d.doc);
if (n == "has") {
need(1);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
return {b.CreateICmpSGE(i, b.getInt64(0)), FType::Bool};
}
if (n == "get") { // m.get(key, default): the default is only computed when the key is missing
need(2);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
BasicBlock *found = newBlock("get.found"), *missing = newBlock("get.missing"), *end = newBlock("get.end");
b.CreateCondBr(b.CreateICmpSGE(i, b.getInt64(0)), found, missing);
b.SetInsertPoint(found);
Value *v = b.CreateLoad(ty(vt), b.CreateStructGEP(ET, mapEntry(map, t, i), 2));
v = copyValue(v, vt);
BasicBlock *foundEnd = b.GetInsertBlock();
b.CreateBr(end);
b.SetInsertPoint(missing);
Value *d = own(coerce(exprWant(*m.args[1], vt), vt, m.args[1]->pos, "the default"));
BasicBlock *missingEnd = b.GetInsertBlock();
b.CreateBr(end);
b.SetInsertPoint(end);
PHINode *phi = b.CreatePHI(ty(vt), 2);
phi->addIncoming(v, foundEnd);
phi->addIncoming(d, missingEnd);
return {phi, vt, false, owning(vt)};
}
if (n == "remove") { // true if the key was there
need(1);
Value_ k = keyArg();
Value *i = b.CreateCall(rt("finch_map_find"), mapArgs(map, tmp(k.v), t));
release(k);
BasicBlock *hit = newBlock("remove.hit"), *end = newBlock("remove.end");
Value *was = b.CreateICmpSGE(i, b.getInt64(0));
b.CreateCondBr(was, hit, end);
b.SetInsertPoint(hit);
Value *entry = mapEntry(map, t, i);
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
b.CreateCall(rt("finch_map_remove_at"), {map, i, b.getInt64(dl->getTypeAllocSize(ET))});
b.CreateBr(end);
b.SetInsertPoint(end);
return {was, FType::Bool};
}
if (n == "clear") {
need(0);
if (owning(kt) || owning(vt))
forEachEntry(map, t, [&](Value *, Value *entry) {
dropAt(b.CreateStructGEP(ET, entry, 1), kt);
dropAt(b.CreateStructGEP(ET, entry, 2), vt);
});
b.CreateCall(rt("finch_map_clear"), {map});
return {nullptr, FType::Void};
}
if (n == "keys" || n == "values") { // a new array, in insertion order
need(0);
FType et = n == "keys" ? kt : vt;
unsigned field = n == "keys" ? 1 : 2;
FType at = FType::arrayOf(et);
Value *out = tmpOf(ty(at));
Value *len = b.CreateLoad(b.getInt64Ty(), b.CreateStructGEP(ty(t), map, 1));
b.CreateCall(rt("finch_arr_make"), {out, len, elemSize(et)});
Value *data = b.CreateLoad(b.getPtrTy(), b.CreateStructGEP(ty(at), out, 0));
Value *j = tmp(b.getInt64(0));
forEachEntry(map, t, [&](Value *, Value *entry) {
Value *jv = b.CreateLoad(b.getInt64Ty(), j);
copyAt(b.CreateInBoundsGEP(ty(et), data, jv), b.CreateStructGEP(ET, entry, field), et);
b.CreateStore(b.CreateAdd(jv, b.getInt64(1)), j);
});
return {b.CreateLoad(ty(at), out), at, false, true};
}
failAt(p.file, p.line, p.col, "a map has no method '" + n + "' (it has: has, get, remove, clear, keys, values; and m[key], .len)");
}
} // namespace finch

View file

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

View file

@ -1,48 +0,0 @@
#pragma once
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
// Every source file the compiler has read; positions point into this list.
struct SourceFile {
std::string path;
std::string text;
};
inline std::vector<SourceFile> g_files;
inline int g_curFile = 0; // the file the lexer/parser is working on
// The language server needs errors as values: it shows them in the editor and keeps running.
struct FinchError {
int file, line, col;
std::string msg;
};
inline bool g_throwErrors = false;
// Finch stops at the first error and shows exactly where it is.
[[noreturn]] inline void failAt(int file, int line, int col, const std::string &msg) {
if (g_throwErrors) throw FinchError{file, line, col, msg};
const SourceFile &f = g_files.at(file);
std::fprintf(stderr, "%s:%d:%d: error: %s\n", f.path.c_str(), line, col, msg.c_str());
size_t start = 0;
for (int l = 1; l < line && start != std::string::npos; l++) {
start = f.text.find('\n', start);
if (start != std::string::npos) start++;
}
if (line > 0 && start != std::string::npos && start < f.text.size()) {
std::string text = f.text.substr(start, f.text.find('\n', start) - start);
std::string pad;
int c = 1; // columns count characters; skip UTF-8 continuation bytes
for (size_t k = 0; k < text.size() && c < col; k++) {
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);
}
[[noreturn]] inline void fail(int line, int col, const std::string &msg) { failAt(g_curFile, line, col, msg); }

View file

@ -362,7 +362,15 @@ fn resolve(ptr[Gen] g, ast.Type t, ast.Node at) -> ast.Type {
return ast.arrayOf(resolve(g, t.elem[0], at)) return ast.arrayOf(resolve(g, t.elem[0], at))
} }
if t.kind == "map" { if t.kind == "map" {
return ast.mapOf(resolve(g, t.elem[0], at), resolve(g, t.elem[1], at)) key := resolve(g, t.elem[0], at)
if !isInt(key) && key.kind != "char" && key.kind != "bool" && key.kind != "str" && key.kind != "enum" {
fail(g, at, "a map key must be a whole number, a char, a bool or a str, not " + ast.show(key))
}
value := resolve(g, t.elem[1], at)
if value.kind == "void" {
fail(g, at, "a map value can't have the type 'nothing'")
}
return ast.mapOf(key, value)
} }
if t.kind == "fixed" { if t.kind == "fixed" {
out := ast.Type(kind: "fixed", count: t.count) out := ast.Type(kind: "fixed", count: t.count)
@ -2917,6 +2925,18 @@ fn badOperands(ptr[Gen] g, str o, Val l, Val r, ast.Node at) {
if o == "+" && (l.ty.kind == "str" || r.ty.kind == "str") { if o == "+" && (l.ty.kind == "str" || r.ty.kind == "str") {
msg = msg + " (turn the other side into text first: str(...))" msg = msg + " (turn the other side into text first: str(...))"
} }
if (o == "&" || o == "|") && l.ty.kind == "bool" {
if o == "&" {
msg = msg + " (for true/false use '&&')"
} else {
msg = msg + " (for true/false use '||')"
}
} else if o == "&" || o == "|" || o == "^" || o == "<<" || o == ">>" {
msg = msg + " (bit operators work on whole numbers only)"
}
if (l.ty.kind == "struct" || l.ty.kind == "array") && (o == "==" || o == "!=") {
msg = msg + " (compare the parts one by one)"
}
fail(g, at, msg) fail(g, at, msg)
} }
@ -3015,6 +3035,9 @@ fn arith(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val {
fn compare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val { fn compare(ptr[Gen] g, str o, Val l, Val r, ast.Node at) -> Val {
eq := o == "==" || o == "!=" eq := o == "==" || o == "!="
if l.ty.kind == "ptr" || r.ty.kind == "ptr" || l.ty.kind == "null" || r.ty.kind == "null" { if l.ty.kind == "ptr" || r.ty.kind == "ptr" || l.ty.kind == "null" || r.ty.kind == "null" {
if l.ty.kind == "str" || r.ty.kind == "str" {
fail(g, at, "a str is never null; to check for empty text use s.len == 0")
}
if !eq { if !eq {
badOperands(g, o, l, r, at) badOperands(g, o, l, r, at)
} }
@ -4125,6 +4148,19 @@ fn runtimeDeclares(str name) -> bool {
return ["printf", "fflush", "exit", "free", "qsort"].contains(name) return ["printf", "fflush", "exit", "free", "qsort"].contains(name)
} }
// Help people who forgot an import.
fn headerHint(str name) -> str {
names := ["printf", "puts", "putchar", "scanf", "fopen", "getchar", "sqrt", "pow", "sin", "cos", "floor", "fabs", "malloc", "rand", "srand",
"abs", "atoi", "strlen", "strcpy", "memcpy", "time", "sleep", "usleep"]
headers := ["stdio.h", "stdio.h", "stdio.h", "stdio.h", "stdio.h", "stdio.h", "math.h", "math.h", "math.h", "math.h", "math.h", "math.h",
"stdlib.h", "stdlib.h", "stdlib.h", "stdlib.h", "stdlib.h", "string.h", "string.h", "string.h", "time.h", "unistd.h", "unistd.h"]
k := names.find(name)
if k < 0 {
return ""
}
return " (it's from C: add import \"" + headers[k] + "\" at the top)"
}
fn call(ptr[Gen] g, ast.Node c) -> Val { fn call(ptr[Gen] g, ast.Node c) -> Val {
n := c.name n := c.name
if n == "error" { if n == "error" {
@ -4285,7 +4321,7 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
checkArgs(g, c, 0, 1, n) checkArgs(g, c, 0, 1, n)
v := expr(g, c.kids[0]) v := expr(g, c.kids[0])
if v.ty.kind != "ptr" { if v.ty.kind != "ptr" {
fail(g, c.kids[0], "free(...) is only for pointers from new(...)") fail(g, c.kids[0], "free(...) is only for pointers from new(...) or C; arrays, str and structs are freed by Finch at the end of their block")
} }
emit(g, "call void @free(ptr " + v.v + ")") emit(g, "call void @free(ptr " + v.v + ")")
return Val(ty: T("void")) return Val(ty: T("void"))
@ -4331,7 +4367,7 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
if lookup(g, n) >= 0 { if lookup(g, n) >= 0 {
fail(g, c, "'" + n + "' is a variable, not a function") fail(g, c, "'" + n + "' is a variable, not a function")
} }
fail(g, c, "there is no function named '" + n + "'") fail(g, c, "there is no function named '" + n + "'" + headerHint(n))
return Val() return Val()
} }
@ -4541,7 +4577,16 @@ fn method(ptr[Gen] g, ast.Node m) -> Val {
} }
} }
if fi < 0 { if fi < 0 {
fail(g, m, "the " + st.kind + " " + ast.show(st) + " has no method '" + m.name + "'") []str have
for sg in g.sigs {
if sg.recv == st.name && sg.recvmod == st.module {
have.push(sg.name)
}
}
if have.len == 0 {
fail(g, m, "the " + st.kind + " " + ast.show(st) + " has no methods; add one with fn " + st.name + "." + m.name + "(...) { ... }")
}
fail(g, m, "the " + st.kind + " " + ast.show(st) + " has no method '" + m.name + "' (it has: " + have.join(", ") + ")")
} }
// a method that changes self can't be called on a temporary or a read-only value // a method that changes self can't be called on a temporary or a read-only value
if g.sigs[fi].mutself && (t.kind == "struct" || t.kind == "enum") { if g.sigs[fi].mutself && (t.kind == "struct" || t.kind == "enum") {

View file

@ -1,39 +0,0 @@
#pragma once
// What the compiler learns about a program while checking it, for the language server:
// what each name is, its type, and where it was defined.
#include <string>
#include <vector>
struct SymRef { // a name in the source
int file, line, col, len;
std::string hover; // markdown shown on mouse-over
int defFile = -1, defLine = 0, defCol = 0;
};
struct VarInfo { // a variable or parameter, for completion
std::string name, type;
int file, line, col;
int fnFile, fnLine, fnEnd; // the function it lives in
};
struct FnInfo {
std::string module, name, signature, doc;
std::vector<std::string> params;
int file, line, col, endLine;
};
struct StructIndex {
std::string module, name;
std::vector<std::pair<std::string, std::string>> fields; // name, type
int file, line, col;
};
struct SemIndex {
std::vector<SymRef> refs;
std::vector<VarInfo> vars;
std::vector<FnInfo> fns;
std::vector<StructIndex> structs;
};
inline SemIndex *g_index = nullptr; // set by the language server; null when compiling

View file

@ -1,214 +0,0 @@
#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}, {"struct", Tok::Struct},
{"defer", Tok::Defer}, {"or", Tok::OrElse}, {"try", Tok::Try},
};
std::vector<Token> lex(int file) {
g_curFile = file;
const std::string &src = g_files[file].text;
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 ':': n == '=' ? two(Tok::Declare) : one(Tok::Colon); break;
case '&': n == '&' ? two(Tok::And) : one(Tok::Amp); break;
case '|': n == '|' ? two(Tok::Or) : one(Tok::Pipe); break;
default: {
// 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::Struct: return "'struct'";
case Tok::Defer: return "'defer'";
case Tok::OrElse: return "'or'";
case Tok::Try: return "'try'";
case Tok::Colon: return "':'";
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 "?";
}

View file

@ -195,7 +195,20 @@ fn lex(str src, str path) -> []Token {
} else { } else {
op := operator(lx) op := operator(lx)
if op.len == 0 { if op.len == 0 {
fail(lx, t.line, t.col, "unexpected character '" + str(c) + "'") // show the whole UTF-8 character, not just its first byte
n := 1
if (int(c) & 0xE0) == 0xC0 {
n = 2
} else if (int(c) & 0xF0) == 0xE0 {
n = 3
} else if (int(c) & 0xF8) == 0xF0 {
n = 4
}
hint := ""
if int(c) >= 0x80 {
hint = " (names use only a-z, A-Z, 0-9 and _; other letters only go inside \"text\")"
}
fail(lx, t.line, t.col, "unexpected character '" + lx.src.sub(lx.i, lx.i + n) + "'" + hint)
} }
for k in 0..op.len { for k in 0..op.len {
advance(lx) advance(lx)

View file

@ -1,36 +0,0 @@
#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, Struct, Defer,
OrElse, Try, // or, try
// punctuation
LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, 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
};
// Lexes g_files[file].text.
std::vector<Token> lex(int file);
const char *tokName(Tok t);

304
src/linker.fch Normal file
View file

@ -0,0 +1,304 @@
// From LLVM IR to a program: compile the IR, add the runtime (compiled once and kept), link the C libraries
// and C files the program asks for, and turn the linker's complaints into something a person can act on.
import platform
import cimport
import "stdlib.h"
import "unistd.h"
// 32-bit FNV-1a, as hex: names the cached runtime object after what it was built from
fn fingerprint(str text) -> str {
u32 h = 2166136261
for c in text {
h = (h ^ u32(c)) * 16777619
}
digits := "0123456789abcdef"
[]str parts
for k in 0..8 {
parts.push(str(digits[int((h >> u32(28 - 4 * k)) & 15)]))
}
return parts.join("")
}
fn quote(str a) -> str {
return "'" + a.replace("'", "'\\''") + "'"
}
// where files that are made once and kept go: ~/.cache/finch
fn cacheDir() -> str {
x := getenv("XDG_CACHE_HOME")
if x.len > 0 {
return x + "/finch"
}
h := getenv("HOME")
if h.len > 0 {
return h + "/.cache/finch"
}
return "/tmp/finch-cache"
}
// "-lname", or whatever pkg-config says the library needs
fn libFlags(str name, platform.Target tg) -> str {
if name.ends_with(".framework") {
return "-framework " + quote(name.sub(0, name.len - 10)) // macOS: link "Cocoa.framework"
}
if !tg.cross {
tmp := "/tmp/finch_pkg_flags.txt"
code := shell("pkg-config --libs " + quote(name) + " > '" + tmp + "' 2> /dev/null")
if code == 0 && file_exists(tmp) {
flags := (read_file(tmp) or "").trim()
shell("rm -f '" + tmp + "'")
if flags.len > 0 {
return flags
}
}
}
return quote("-l" + name)
}
// "GLFW/glfw3.h" -> "glfw": a decent first guess for the library's name
fn guessLib(str header) -> str {
name := header
slash := name.find("/")
while slash >= 0 {
name = name.sub(slash + 1, name.len)
slash = name.find("/")
}
dot := name.len
for i in 0..name.len {
if name[i] == '.' {
dot = i
}
}
name = name.sub(0, dot)
while name.len > 0 && name[name.len - 1] >= '0' && name[name.len - 1] <= '9' {
name = name.sub(0, name.len - 1)
}
return name.lower()
}
struct Names {
[]str items
}
fn Names.add(str x) {
if x.len > 0 && !self.items.contains(x) {
self.items.push(x)
}
}
// the text after `marker` up to the next one of `stops`
fn between(str line, str marker, str stops) -> str {
a := line.find(marker)
if a < 0 {
return ""
}
rest := line.sub(a + marker.len, line.len)
n := 0
while n < rest.len && !stops.contains(str(rest[n])) {
n += 1
}
return rest.sub(0, n)
}
// The linker speaks in riddles; say what is missing and what to write.
fn linkFailed(str output, cimport.CImports ci, platform.Target tg) -> int {
missing := Names()
libs := Names()
for line in output.split("\n") {
l := line.trim()
if l.contains("undefined reference to `") {
missing.add(between(l, "undefined reference to `", "'"))
} else if l.contains("undefined symbol: ") {
missing.add(between(l, "undefined symbol: ", " \n"))
} else if l.contains("unresolved external symbol ") {
missing.add(between(l, "unresolved external symbol ", " \n\r"))
} else if l.contains("cannot find -l") {
libs.add(between(l, "cannot find -l", ": \n\r"))
} else if l.contains("library not found for -l") {
libs.add(between(l, "library not found for -l", ": \n\r"))
} else if l.contains("ld: library '") {
libs.add(between(l, "ld: library '", "'"))
} else if l.contains("framework '") && l.contains("not found") {
libs.add(between(l, "framework '", "'") + ".framework")
} else if l.starts_with("\"") && l.contains("\", referenced from:") {
// Apple ld: "_name", referenced from:
name := l.sub(1, l.find("\", referenced from:"))
if tg.darwin && name.starts_with("_") {
name = name.sub(1, name.len)
}
missing.add(name)
}
}
if missing.items.len == 0 && libs.items.len == 0 {
print(output + "error: linking failed")
return 1
}
for lib in libs.items {
print("error: the library '" + lib + "' wasn't found. Is it installed? (on Nix: add it to shell.nix)")
}
// the missing functions, grouped by the header they came from
[]str headers
[]Names byHeader
for name in missing.items {
header := ""
if ci.fns.has(name) {
header = ci.fns[name].header
}
k := headers.find(header)
if k < 0 {
headers.push(header)
byHeader.push(Names())
k = headers.len - 1
}
byHeader[k].add(name)
}
for k in 0..headers.len {
names := byHeader[k].items
[]str shown
for i in 0..names.len {
if i < 3 {
shown.push(names[i])
}
}
list := shown.join(", ")
if names.len > 3 {
list = list + " and " + str(names.len - 3) + " more"
}
if headers[k].len == 0 {
print("error: no code found for " + list)
continue
}
verb := "come"
if names.len == 1 {
verb = "comes"
}
print("error: " + list + " " + verb + " from \"" + headers[k] + "\", but its library isn't linked")
print(" the header only says the functions exist; their code lives in a library.")
print(" add this at the top of your file (with the library's real name):")
print("")
print(" link \"" + guessLib(headers[k]) + "\"")
print("")
}
return 1
}
// The runtime, compiled once per system and compiler and kept in the cache folder.
fn runtimeObject(str rt, platform.Target tg, str cc) -> str! {
source := try read_file(rt)
dir := cacheDir()
obj := dir + "/rt-" + fingerprint(source + cc + tg.triple + "2.5") + ".o"
if file_exists(obj) {
return obj
}
shell("mkdir -p " + quote(dir))
part := obj + ".part" + str(int(getpid()))
pic := " -fPIC"
if tg.windows {
pic = ""
}
log := part + ".log"
code := shell(cc + " -O2" + pic + " -c " + quote(rt) + " -o " + quote(part) + " > " + quote(log) + " 2>&1")
if code != 0 {
text := read_file(log) or ""
shell("rm -f " + quote(part) + " " + quote(log))
return error(text + "error: couldn't build the Finch runtime with '" + cc + "' (is a C compiler installed?)")
}
shell("rm -f " + quote(log) + " && mv " + quote(part) + " " + quote(obj))
return obj
}
struct Build {
str out // the program
str ir // its LLVM IR, as text
str rt // the runtime's source file
str opt // -O2 or -O0
str extra // -g, -fPIC ...
bool shared
[]str links // what the program asked to link: a library name, a .c file (with its folder), a .o or .a
}
// IR -> program. 0 if it worked.
fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
clang := getenv("FINCH_CLANG")
if clang.len == 0 {
clang = "clang"
}
// the C compiler of the runtime and the linker: the target's own when building for another system
cc := clang
if tg.cross {
cc = tg.cc
} else if getenv("FINCH_CC").len > 0 {
cc = getenv("FINCH_CC")
}
rtobj := runtimeObject(b.rt, tg, cc) or {
print(err)
return 1
}
ll := b.out + ".ll"
obj := b.out + ".o"
write_file(ll, b.ir)
target := ""
if tg.cross {
target = " --target=" + tg.triple
}
log := b.out + ".log"
code := shell(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1")
shell("rm -f " + quote(ll))
if code != 0 {
print(read_file(log) or "")
print("error: clang couldn't compile the program's IR")
shell("rm -f " + quote(obj) + " " + quote(log))
return 1
}
cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
if !tg.msvc {
cmd = cmd + " -lm"
}
if b.shared {
if tg.darwin {
cmd = cmd + " -dynamiclib"
} else {
cmd = cmd + " -shared"
}
}
[]str temps
for l in b.links {
if l.ends_with(".c") {
// the program's own C file, built along with it
o := b.out + "." + str(temps.len) + ".o"
pic := " -fPIC"
if tg.windows {
pic = ""
}
c := shell(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1")
if c != 0 {
print((read_file(log) or "") + "error: couldn't compile " + l)
shell("rm -f " + quote(obj) + " " + quote(log) + " " + quote(o))
return 1
}
temps.push(o)
cmd = cmd + " " + quote(o)
} else if l.ends_with(".o") || l.ends_with(".a") {
cmd = cmd + " " + quote(l)
} else {
cmd = cmd + " " + libFlags(l, tg)
}
}
status := shell(cmd + " > " + quote(log) + " 2>&1")
output := read_file(log) or ""
shell("rm -f " + quote(obj) + " " + quote(log))
for t in temps {
shell("rm -f " + quote(t))
}
if status == 127 {
print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)")
return 1
}
if status != 0 {
return linkFailed(output, ci, tg)
}
if output.len > 0 {
print(output) // warnings, if any
}
return 0
}

View file

@ -1,74 +0,0 @@
#include "loader.h"
#include "error.h"
#include "lexer.h"
#include "parser.h"
#include "target.h"
#include <llvm/ADT/SmallString.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
using namespace llvm;
static bool readFile(const std::string &path, std::string &out) {
std::ifstream in(path, std::ios::binary);
if (!in) return false;
std::stringstream ss;
ss << in.rdbuf();
out = ss.str();
return true;
}
std::string normalizePath(const std::string &path) {
SmallString<256> p(path);
sys::fs::make_absolute(p);
sys::path::remove_dots(p, true);
sys::path::native(p);
return std::string(p.str());
}
void Loader::load(const std::string &path, const std::string &module, Pos from) {
std::string text;
bool found = false;
if (overrides) {
auto it = overrides->find(normalizePath(path));
if (it != overrides->end()) text = it->second, found = true;
}
if (!found && !readFile(path, text)) {
if (from.line) failAt(from.file, from.line, from.col, "can't find the module '" + module + "' (looked for " + path + ")");
std::fprintf(stderr, "error: can't open '%s'\n", path.c_str());
std::exit(1);
}
int idx = (int)g_files.size();
g_files.push_back({path, text});
Program p = parse(lex(idx), idx);
p.module = module;
p.path = path;
std::vector<Import> mods;
for (const Import &im : p.imports)
if (!im.isC) mods.push_back(im);
progs.push_back(std::move(p));
for (const Import &im : mods) {
if (im.path == "main") failAt(im.pos.file, im.pos.line, im.pos.col, "'main' can't be imported");
if (loaded[im.path]) continue;
loaded[im.path] = true;
load(findModule(dirOf(path), im.path), im.path, im.pos);
}
}
// next to the importing file, then in $FINCH_PATH folders
std::string Loader::findModule(const std::string &dir, const std::string &name) {
std::string here = dir + "/" + name + ".fch";
if (sys::fs::exists(here)) return here;
if (const char *fp = std::getenv("FINCH_PATH")) {
std::stringstream ss(fp);
for (std::string d; std::getline(ss, d, pathListSeparator());)
if (!d.empty() && sys::fs::exists(d + "/" + name + ".fch")) return d + "/" + name + ".fch";
}
return here;
}

View file

@ -1,20 +0,0 @@
#pragma once
// Reads the main file and, recursively, every Finch module it imports.
#include "ast.h"
#include <map>
#include <string>
#include <vector>
struct Loader {
std::vector<Program> progs;
std::map<std::string, bool> loaded; // module name -> seen
const std::map<std::string, std::string> *overrides = nullptr; // path -> text (files open in an editor)
void load(const std::string &path, const std::string &module, Pos from);
static std::string findModule(const std::string &dir, const std::string &name);
};
// A path made absolute and tidy, so two spellings of one file compare equal.
std::string normalizePath(const std::string &path);

View file

@ -1,580 +0,0 @@
// The Finch language server: `finch lsp` speaks the Language Server Protocol on stdin/stdout.
//
// Every time a document changes it is checked by the real compiler (lexer, parser, type
// checker and code generator, without optimizing or writing anything), with errors thrown
// instead of ending the process and a semantic index (index.h) collecting what each name is.
// Hover, go-to-definition, completion, signature help and document symbols read that index.
#include "builtins_doc.h"
#include "cimport.h"
#include "codegen.h"
#include "error.h"
#include "index.h"
#include "loader.h"
#include "target.h"
#include <llvm/MC/TargetRegistry.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/JSON.h>
#include <llvm/Support/Path.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Target/TargetMachine.h>
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#endif
using namespace llvm;
namespace {
// ---------- URIs ----------
std::string uriToPath(const std::string &uri) {
std::string s = uri.rfind("file://", 0) == 0 ? uri.substr(7) : uri;
std::string out;
for (size_t i = 0; i < s.size(); i++) {
if (s[i] == '%' && i + 2 < s.size()) {
out += (char)std::stoi(s.substr(i + 1, 2), nullptr, 16);
i += 2;
} else {
out += s[i];
}
}
#ifdef _WIN32
if (out.size() > 2 && out[0] == '/' && out[2] == ':') out = out.substr(1); // /c:/x -> c:/x
#endif
return normalizePath(out);
}
std::string pathToUri(const std::string &path) {
std::string p = normalizePath(path), out = "file://";
#ifdef _WIN32
for (char &c : p)
if (c == '\\') c = '/';
out += "/";
#endif
for (unsigned char c : p) {
if (isalnum(c) || c == '/' || c == '-' || c == '_' || c == '.' || c == '~' || c == ':') out += (char)c;
else {
char buf[4];
std::snprintf(buf, sizeof buf, "%%%02X", c);
out += buf;
}
}
return out;
}
// ---------- one check of a document ----------
struct Diagnostic {
std::string path;
int line, col;
std::string msg;
};
struct Analysis {
std::vector<SourceFile> files;
SemIndex index;
std::vector<Diagnostic> diags;
std::set<std::string> modules; // modules the document imports
const CImports *c = nullptr;
};
class Server {
public:
int run();
private:
std::map<std::string, std::string> docs; // normalized path -> text
std::map<std::string, std::shared_ptr<Analysis>> results; // last check of each open document
std::map<std::string, std::shared_ptr<Analysis>> lastGood; // last check without errors
std::map<std::string, std::unique_ptr<CImports>> headerCache;
std::set<std::string> diagnosed; // paths we published errors for
TargetMachine *tm = nullptr;
bool shutdown = false;
// protocol
bool readMessage(json::Value &out);
void send(const json::Value &v);
void reply(const json::Value &id, json::Value result);
void notify(const std::string &method, json::Value params);
void handle(const json::Object &msg);
// features
void check(const std::string &path);
json::Value hover(const std::string &path, int line, int col);
json::Value definition(const std::string &path, int line, int col);
json::Value completion(const std::string &path, int line, int col);
json::Value symbols(const std::string &path);
json::Value signatureHelp(const std::string &path, int line, int col);
const SymRef *refAt(const Analysis &a, const std::string &path, int line, int col);
std::string lineText(const std::string &path, int line);
};
bool Server::readMessage(json::Value &out) {
size_t length = 0;
std::string header;
while (std::getline(std::cin, header)) {
if (!header.empty() && header.back() == '\r') header.pop_back();
if (header.empty()) break;
if (header.rfind("Content-Length:", 0) == 0) length = std::stoul(header.substr(15));
}
if (!std::cin || length == 0) return false;
std::string body(length, '\0');
std::cin.read(&body[0], length);
Expected<json::Value> v = json::parse(body);
if (!v) {
consumeError(v.takeError());
return true; // skip a broken message
}
out = std::move(*v);
return true;
}
void Server::send(const json::Value &v) {
std::string body;
raw_string_ostream os(body);
os << v;
os.flush();
std::cout << "Content-Length: " << body.size() << "\r\n\r\n" << body;
std::cout.flush();
}
void Server::reply(const json::Value &id, json::Value result) {
send(json::Object{{"jsonrpc", "2.0"}, {"id", id}, {"result", std::move(result)}});
}
void Server::notify(const std::string &method, json::Value params) {
send(json::Object{{"jsonrpc", "2.0"}, {"method", method}, {"params", std::move(params)}});
}
int Server::run() {
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
#endif
initTargets();
setTarget("");
std::string err;
const Target *target = TargetRegistry::lookupTarget(g_target.triple, err);
if (!target) return 1;
tm = target->createTargetMachine(g_target.triple, "generic", "", TargetOptions(), Reloc::PIC_);
json::Value msg(nullptr);
while (readMessage(msg)) {
if (const json::Object *o = msg.getAsObject()) handle(*o);
if (shutdown && !std::cin) break;
}
return 0;
}
static int intAt(const json::Object *o, const char *key) {
if (!o) return 0;
if (auto v = o->getInteger(key)) return (int)*v;
return 0;
}
void Server::handle(const json::Object &msg) {
std::string method = msg.getString("method").value_or("").str();
const json::Value *id = msg.get("id");
const json::Object *params = msg.getObject("params");
auto docPath = [&]() -> std::string {
if (!params) return "";
const json::Object *td = params->getObject("textDocument");
return td ? uriToPath(td->getString("uri").value_or("").str()) : "";
};
// positions: LSP is 0-based (line, UTF-16 column); Finch is 1-based (line, character)
auto position = [&](int &line, int &col) {
const json::Object *pos = params ? params->getObject("position") : nullptr;
line = intAt(pos, "line") + 1;
col = intAt(pos, "character") + 1;
};
if (method == "initialize") {
json::Object caps{
{"textDocumentSync", json::Object{{"openClose", true}, {"change", 1}, {"save", true}}},
{"hoverProvider", true},
{"definitionProvider", true},
{"documentSymbolProvider", true},
{"completionProvider", json::Object{{"triggerCharacters", json::Array{"."}}}},
{"signatureHelpProvider", json::Object{{"triggerCharacters", json::Array{"(", ","}}}},
};
reply(*id, json::Object{{"capabilities", std::move(caps)},
{"serverInfo", json::Object{{"name", "finch"}, {"version", FINCH_VERSION}}}});
} else if (method == "shutdown") {
shutdown = true;
reply(*id, nullptr);
} else if (method == "exit") {
std::exit(shutdown ? 0 : 1);
} else if (method == "textDocument/didOpen" || method == "textDocument/didChange") {
std::string path = docPath();
if (method == "textDocument/didOpen") {
docs[path] = params->getObject("textDocument")->getString("text").value_or("").str();
} else if (const json::Array *changes = params->getArray("contentChanges")) {
if (!changes->empty())
if (const json::Object *ch = changes->back().getAsObject()) docs[path] = ch->getString("text").value_or("").str();
}
check(path);
} else if (method == "textDocument/didSave") {
check(docPath());
} else if (method == "textDocument/didClose") {
std::string path = docPath();
docs.erase(path);
results.erase(path);
lastGood.erase(path);
notify("textDocument/publishDiagnostics", json::Object{{"uri", pathToUri(path)}, {"diagnostics", json::Array{}}});
} else if (id) {
int line = 0, col = 0;
position(line, col);
std::string path = docPath();
if (method == "textDocument/hover") reply(*id, hover(path, line, col));
else if (method == "textDocument/definition") reply(*id, definition(path, line, col));
else if (method == "textDocument/completion") reply(*id, completion(path, line, col));
else if (method == "textDocument/documentSymbol") reply(*id, symbols(path));
else if (method == "textDocument/signatureHelp") reply(*id, signatureHelp(path, line, col));
else send(json::Object{{"jsonrpc", "2.0"}, {"id", *id},
{"error", json::Object{{"code", -32601}, {"message", "not supported: " + method}}}});
}
}
// Check `path` with the real compiler, keep the index, publish errors.
void Server::check(const std::string &path) {
auto a = std::make_shared<Analysis>();
g_files.clear();
g_throwErrors = true;
g_index = &a->index;
try {
Loader loader;
loader.overrides = &docs;
loader.load(path, "", Pos{});
std::vector<Import> imports;
std::vector<std::string> dirs;
std::string key;
for (const Program &p : loader.progs) {
for (const Import &im : p.imports) {
imports.push_back(im);
if (im.isC) key += im.path + "\n";
else if (p.module.empty()) a->modules.insert(im.path);
}
std::string d = dirOf(p.path);
if (std::find(dirs.begin(), dirs.end(), d) == dirs.end()) dirs.push_back(d), key += "@" + d + "\n";
}
auto &cached = headerCache[key]; // headers are slow to read: once per set of imports
if (!cached) cached = std::make_unique<CImports>(importHeaders(imports, dirs));
a->c = cached.get();
LLVMContext ctx;
generate(loader.progs, *cached, ctx, *tm, false);
} catch (const FinchError &e) {
std::string file = e.file >= 0 && e.file < (int)g_files.size() ? normalizePath(g_files[e.file].path) : path;
a->diags.push_back({file, e.line, e.col, e.msg});
}
g_index = nullptr;
g_throwErrors = false;
a->files = g_files;
// While the code is half-typed (`p.`, `add(1, `) it doesn't compile; completion and signature
// help keep using what the last good version knew. Hover and definition use the newest refs.
if (a->diags.empty()) {
lastGood[path] = a;
} else if (auto good = lastGood.find(path); good != lastGood.end()) {
const Analysis &g = *good->second;
for (const FnInfo &f : g.index.fns) a->index.fns.push_back(f);
for (const StructIndex &st : g.index.structs) a->index.structs.push_back(st);
for (const VarInfo &v : g.index.vars) a->index.vars.push_back(v);
if (a->modules.empty()) a->modules = g.modules;
if (!a->c) a->c = g.c;
}
results[path] = a;
std::map<std::string, json::Array> byFile;
byFile[path];
for (const std::string &p : diagnosed) byFile[p]; // clear errors that went away
diagnosed.clear();
for (const Diagnostic &d : a->diags) {
int line = std::max(0, d.line - 1), col = std::max(0, d.col - 1);
byFile[d.path].push_back(json::Object{
{"range", json::Object{{"start", json::Object{{"line", line}, {"character", col}}},
{"end", json::Object{{"line", line}, {"character", col + 1}}}}},
{"severity", 1},
{"source", "finch"},
{"message", d.msg}});
diagnosed.insert(d.path);
}
for (auto &[p, list] : byFile)
notify("textDocument/publishDiagnostics", json::Object{{"uri", pathToUri(p)}, {"diagnostics", std::move(list)}});
}
const SymRef *Server::refAt(const Analysis &a, const std::string &path, int line, int col) {
for (const SymRef &r : a.index.refs) {
if (r.line != line || col < r.col || col > r.col + r.len) continue;
if (r.file < 0 || r.file >= (int)a.files.size() || normalizePath(a.files[r.file].path) != path) continue;
return &r;
}
return nullptr;
}
json::Value Server::hover(const std::string &path, int line, int col) {
auto it = results.find(path);
if (it == results.end()) return nullptr;
const SymRef *r = refAt(*it->second, path, line, col);
if (!r) return nullptr;
return json::Object{{"contents", json::Object{{"kind", "markdown"}, {"value", r->hover}}}};
}
json::Value Server::definition(const std::string &path, int line, int col) {
auto it = results.find(path);
if (it == results.end()) return nullptr;
const Analysis &a = *it->second;
const SymRef *r = refAt(a, path, line, col);
if (!r || r->defFile < 0 || r->defFile >= (int)a.files.size()) return nullptr;
int l = r->defLine - 1, c = r->defCol - 1;
return json::Object{{"uri", pathToUri(a.files[r->defFile].path)},
{"range", json::Object{{"start", json::Object{{"line", l}, {"character", c}}},
{"end", json::Object{{"line", l}, {"character", c}}}}}};
}
std::string Server::lineText(const std::string &path, int line) {
auto it = docs.find(path);
if (it == docs.end()) return "";
size_t start = 0;
for (int l = 1; l < line && start != std::string::npos; l++) {
start = it->second.find('\n', start);
if (start != std::string::npos) start++;
}
if (start == std::string::npos) return "";
return it->second.substr(start, it->second.find('\n', start) - start);
}
// LSP CompletionItemKind
enum { KMethod = 2, KFunction = 3, KField = 5, KVariable = 6, KModule = 9, KKeyword = 14, KSnippet = 15,
KConstant = 21, KStruct = 22, KTypeParam = 25 };
static json::Value item(const std::string &label, int kind, const std::string &detail = "", const std::string &doc = "") {
json::Object o{{"label", label}, {"kind", kind}};
if (!detail.empty()) o["detail"] = detail;
if (!doc.empty()) o["documentation"] = json::Object{{"kind", "markdown"}, {"value", doc}};
return o;
}
json::Value Server::completion(const std::string &path, int line, int col) {
json::Array items;
auto it = results.find(path);
std::shared_ptr<Analysis> a = it == results.end() ? nullptr : it->second;
// what's before the cursor: "name." or "name.partial"?
std::string text = lineText(path, line);
// columns count characters, but we only need the ASCII part right before the cursor
std::string before;
{
int c = 1;
size_t k = 0;
for (; k < text.size() && c < col; k++)
if (((unsigned char)text[k] & 0xC0) != 0x80) c++;
before = text.substr(0, k);
}
size_t end = before.size();
while (end > 0 && (isalnum((unsigned char)before[end - 1]) || before[end - 1] == '_')) end--;
bool afterDot = end > 0 && before[end - 1] == '.';
if (afterDot) {
size_t s = end - 1;
while (s > 0 && (isalnum((unsigned char)before[s - 1]) || before[s - 1] == '_' || before[s - 1] == ']' || before[s - 1] == '[')) s--;
std::string owner = before.substr(s, end - 1 - s);
if (size_t br = owner.find('['); br != std::string::npos) owner = owner.substr(0, br) + "[]";
if (!a) return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
// a module?
if (a->modules.count(owner)) {
std::set<std::string> seen;
for (const FnInfo &f : a->index.fns)
if (f.module == owner && seen.insert(f.name).second) items.push_back(item(f.name, KFunction, f.signature));
for (const StructIndex &s2 : a->index.structs)
if (s2.module == owner && seen.insert(s2.name).second) items.push_back(item(s2.name, KStruct, "struct " + s2.name));
return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
}
// a variable: find its type (the innermost one declared before the cursor)
bool element = owner.size() > 2 && owner.compare(owner.size() - 2, 2, "[]") == 0;
std::string name = element ? owner.substr(0, owner.size() - 2) : owner;
std::string type;
for (const VarInfo &v : a->index.vars)
if (v.name == name && v.line <= line && line <= v.fnEnd + 1) type = v.type;
if (element && type.rfind("[]", 0) == 0) type = type.substr(2);
if (element && type == "str") type = "char";
if (element && type.rfind("map[", 0) == 0) { // m[key]. -> the value type
int depth = 0;
for (size_t k = 3; k < type.size(); k++) {
if (type[k] == '[') depth++;
else if (type[k] == ']' && --depth == 0) {
type = type.substr(k + 1);
break;
}
}
}
if (type.rfind("ptr[", 0) == 0) type = type.substr(4, type.size() - 5); // fields through a pointer
if (type == "str") {
for (const BuiltinDoc &d : kStrMethods) items.push_back(item(d.name, d.name[0] == 'l' || d.name[0] == 'p' ? KField : KMethod, d.signature, d.doc));
} else if (type.rfind("[]", 0) == 0) {
for (const BuiltinDoc &d : kArrayMethods) items.push_back(item(d.name, std::string(d.name) == "len" || std::string(d.name) == "ptr" ? KField : KMethod, d.signature, d.doc));
} else if (type.rfind("map[", 0) == 0) {
for (const BuiltinDoc &d : kMapMethods) items.push_back(item(d.name, std::string(d.name) == "len" ? KField : KMethod, d.signature, d.doc));
} else if (!type.empty()) {
std::string mod, sname = type;
if (size_t dot = type.find('.'); dot != std::string::npos) mod = type.substr(0, dot), sname = type.substr(dot + 1);
for (const StructIndex &s2 : a->index.structs)
if (s2.name == sname && (mod.empty() || s2.module == mod))
for (auto &[fname, ftype] : s2.fields) items.push_back(item(fname, KField, ftype + " " + fname));
std::set<std::string> seen;
for (const FnInfo &f : a->index.fns) // methods: indexed as Struct.name
if (f.name.size() > sname.size() + 1 && f.name.compare(0, sname.size() + 1, sname + ".") == 0 &&
seen.insert(f.name).second)
items.push_back(item(f.name.substr(sname.size() + 1), KMethod, f.signature));
if (type.rfind("ptr", 0) == 0 || items.empty()) items.push_back(item("value", KField, "the value the pointer points to"));
}
return json::Object{{"isIncomplete", false}, {"items", std::move(items)}};
}
std::string prefix = before.substr(end);
for (const char *k : kKeywords) items.push_back(item(k, KKeyword));
for (const char *t : kTypeNames) items.push_back(item(t, KTypeParam, "type"));
for (const BuiltinDoc &d : kBuiltins) items.push_back(item(d.name, KFunction, d.signature, d.doc));
if (a) {
std::set<std::string> seen;
for (const VarInfo &v : a->index.vars) // variables of the function around the cursor
if (v.fnLine <= line && line <= v.fnEnd && v.line <= line && seen.insert(v.name).second)
items.push_back(item(v.name, KVariable, v.type + " " + v.name));
for (const FnInfo &f : a->index.fns)
if (f.module.empty() && seen.insert(f.name).second) items.push_back(item(f.name, KFunction, f.signature));
for (const StructIndex &s : a->index.structs)
if (s.module.empty() && seen.insert(s.name).second) items.push_back(item(s.name, KStruct, "struct " + s.name));
for (const std::string &m : a->modules) items.push_back(item(m, KModule, "module " + m));
// C names only once something is typed: headers declare thousands
if (a->c && !prefix.empty()) {
for (auto &[name, f] : a->c->fns)
if (name.rfind(prefix, 0) == 0 && f.unsupported.empty()) items.push_back(item(name, KFunction, "C: " + f.header));
for (auto &[name, k] : a->c->consts)
if (name.rfind(prefix, 0) == 0) items.push_back(item(name, KConstant, "C constant"));
for (auto &[name, s] : a->c->structs)
if (name.rfind(prefix, 0) == 0) items.push_back(item(name, KStruct, "C struct"));
}
}
return json::Object{{"isIncomplete", !prefix.empty()}, {"items", std::move(items)}};
}
json::Value Server::symbols(const std::string &path) {
json::Array out;
auto it = results.find(path);
if (it == results.end()) return out;
const Analysis &a = *it->second;
auto inDoc = [&](int file) { return file >= 0 && file < (int)a.files.size() && normalizePath(a.files[file].path) == path; };
auto range = [](int l1, int c1, int l2) {
return json::Object{{"start", json::Object{{"line", l1 - 1}, {"character", c1 - 1}}},
{"end", json::Object{{"line", std::max(l1, l2) - 1}, {"character", 0}}}};
};
for (const FnInfo &f : a.index.fns)
if (inDoc(f.file))
out.push_back(json::Object{{"name", f.name}, {"detail", f.signature}, {"kind", 12},
{"range", range(f.line, f.col, f.endLine + 1)}, {"selectionRange", range(f.line, f.col, f.line)}});
for (const StructIndex &s : a.index.structs)
if (inDoc(s.file)) {
json::Array kids;
for (auto &[n, t] : s.fields) kids.push_back(json::Object{{"name", n}, {"detail", t}, {"kind", 8},
{"range", range(s.line, s.col, s.line)}, {"selectionRange", range(s.line, s.col, s.line)}});
out.push_back(json::Object{{"name", s.name}, {"kind", 23}, {"range", range(s.line, s.col, s.line)},
{"selectionRange", range(s.line, s.col, s.line)}, {"children", std::move(kids)}});
}
return out;
}
// Inside `name(a, b|`: which function, and which argument.
json::Value Server::signatureHelp(const std::string &path, int line, int col) {
auto it = results.find(path);
std::shared_ptr<Analysis> a = it == results.end() ? nullptr : it->second;
std::string text;
for (int l = std::max(1, line - 5); l < line; l++) text += lineText(path, l) + "\n";
std::string cur = lineText(path, line);
text += cur.substr(0, std::min<size_t>(cur.size(), col - 1));
int depth = 0, commas = 0;
size_t k = text.size();
while (k > 0) {
char ch = text[--k];
if (ch == ')' || ch == ']') depth++;
else if (ch == '[') depth--;
else if (ch == '(') {
if (depth == 0) break;
depth--;
} else if (ch == ',' && depth == 0) commas++;
if (k == 0) return nullptr;
}
size_t e = k;
while (k > 0 && (isalnum((unsigned char)text[k - 1]) || text[k - 1] == '_' || text[k - 1] == '.')) k--;
std::string callee = text.substr(k, e - k);
if (callee.empty()) return nullptr;
std::string name = callee.substr(callee.rfind('.') == std::string::npos ? 0 : callee.rfind('.') + 1);
std::string mod = callee.find('.') == std::string::npos ? "" : callee.substr(0, callee.find('.'));
std::string label, doc;
std::vector<std::string> params;
if (a) {
for (const FnInfo &f : a->index.fns) {
bool method = f.name.size() > name.size() && f.name.compare(f.name.size() - name.size() - 1, name.size() + 1, "." + name) == 0;
if ((f.name == name && (f.module == mod || (mod.empty() && f.module.empty()) || !a->modules.count(mod))) ||
(method && !mod.empty() && !a->modules.count(mod))) // p.move( -> Point.move
label = f.signature, params = f.params;
}
if (label.empty())
for (const StructIndex &s : a->index.structs)
if (s.name == name) {
label = name + "(";
for (size_t i = 0; i < s.fields.size(); i++) {
params.push_back(s.fields[i].second + " " + s.fields[i].first);
label += (i ? ", " : "") + params.back();
}
label += ")";
}
if (label.empty() && a->c)
if (auto f = a->c->fns.find(name); f != a->c->fns.end()) {
label = name + "(";
for (size_t i = 0; i < f->second.params.size(); i++) {
params.push_back(f->second.params[i].show());
label += (i ? ", " : "") + params.back();
}
label += f->second.variadic ? ", ...)" : ")";
doc = "C function from " + f->second.header;
}
}
if (label.empty()) {
auto look = [&](const BuiltinDoc *f, const BuiltinDoc *l) {
for (; f != l && label.empty(); ++f)
if (name == f->name) label = f->signature, doc = f->doc;
};
look(std::begin(kBuiltins), std::end(kBuiltins));
if (!mod.empty()) {
look(std::begin(kArrayMethods), std::end(kArrayMethods));
look(std::begin(kStrMethods), std::end(kStrMethods));
look(std::begin(kMapMethods), std::end(kMapMethods));
}
}
if (label.empty()) return nullptr;
json::Array ps;
for (const std::string &p : params) ps.push_back(json::Object{{"label", p}});
json::Object sig{{"label", label}, {"parameters", std::move(ps)}};
if (!doc.empty()) sig["documentation"] = doc;
return json::Object{{"signatures", json::Array{std::move(sig)}}, {"activeSignature", 0}, {"activeParameter", commas}};
}
} // namespace
int runLanguageServer() { return Server().run(); }

View file

@ -1,350 +0,0 @@
#include "cimport.h"
#include "codegen.h"
#include "error.h"
#include "lexer.h"
#include "loader.h"
#include "parser.h"
#include "target.h"
#include <llvm/Config/llvm-config.h>
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/MC/TargetRegistry.h>
#include <llvm/Passes/PassBuilder.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/Path.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Target/TargetMachine.h>
#include <llvm/TargetParser/Host.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include "rt_source.inc" // kRuntimeSource: runtime/finch_rt.c, embedded at build time
using namespace llvm;
static void usage() {
std::fprintf(stderr,
"Finch " FINCH_VERSION " - small, quick, sharp.\n\n"
"usage:\n"
" finch run <file.fch> [args...] compile and run\n"
" finch build <file.fch> [-o name] compile to a program\n"
" finch ir <file.fch> show the generated LLVM IR\n"
" finch version show the version\n"
" finch lsp the language server, for editors (VS Code, Kate, ...)\n"
"\n"
" -l <lib> link a C library, same as link \"lib\" in the file\n"
" --target <name> build for another system: windows, linux, arm64 (Linux), macos,\n"
" or an LLVM triple\n"
" -g add debug info (gdb / lldb / Visual Studio)\n"
" -O0 skip optimizations (to read the raw IR)\n");
std::exit(1);
}
// ---------- LLVM ----------
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 *targetMachine() {
initTargets();
const Triple &triple = g_target.triple;
if (!triple.isX86() && !triple.isAArch64()) {
std::fprintf(stderr, "error: Finch generates code for x86-64 and ARM64, not for %s\n", triple.getArchName().str().c_str());
std::exit(1);
}
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);
}
// ---------- linking ----------
// The runtime is compiled once per version and kept in ~/.cache/finch.
static std::string runtimeObject() {
std::string dir = cacheDir();
size_t hash = std::hash<std::string>()(std::string(kRuntimeSource) + FINCH_VERSION + g_target.cc + g_target.triple.str());
std::string obj = dir + (g_target.windows && !g_target.cross ? "\\" : "/") + "rt-" + std::to_string(hash) + ".o";
if (sys::fs::exists(obj)) return obj;
sys::fs::create_directories(dir);
std::string src = obj + ".c", part = obj + ".part";
{
std::ofstream out(src);
out << kRuntimeSource;
}
std::string output;
std::string pic = g_target.windows ? "" : " -fPIC";
int status = capture(g_target.cc + " -O2" + pic + " -c " + shellQuote(src) + " -o " + shellQuote(part), output);
std::remove(src.c_str());
if (status != 0) {
std::fprintf(stderr, "%serror: couldn't build the Finch runtime with '%s' (is a C compiler installed?)\n", output.c_str(),
g_target.cc.c_str());
std::exit(1);
}
sys::fs::rename(part, obj);
return obj;
}
// pkg-config knows the right paths for a library if it has one; otherwise plain -l<name>.
static std::string libFlags(const std::string &lib) {
std::string out;
// macOS: link "Cocoa.framework" -> -framework Cocoa
if (lib.size() > 10 && lib.compare(lib.size() - 10, 10, ".framework") == 0)
return " -framework " + shellQuote(lib.substr(0, lib.size() - 10));
if (!g_target.cross && capture("pkg-config --libs " + shellQuote(lib), out) == 0) {
while (!out.empty() && std::isspace((unsigned char)out.back())) out.pop_back();
return " " + out;
}
return " " + shellQuote("-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("unresolved external symbol ")) != std::string::npos) { // MSVC link.exe
a += 27;
addOnce(missing, l.substr(a, l.find_first_of(" \n\r", a) - a));
} else if ((a = l.find("cannot open file '")) != std::string::npos || // MSVC: LNK1104
(a = l.find("cannot open input file '")) != std::string::npos || // MSVC: LNK1181
(a = l.find("could not open '")) != std::string::npos) { // lld-link
a = l.find('\'', a) + 1;
std::string lib = l.substr(a, l.find('\'', a) - a);
if (lib.size() > 4 && lib.compare(lib.size() - 4, 4, ".lib") == 0) lib = lib.substr(0, lib.size() - 4);
addOnce(missingLibs, lib);
} else if ((a = l.find("cannot find -l")) != std::string::npos ||
(a = l.find("library not found for -l")) != std::string::npos) { // older Apple ld
a = l.find("-l", a) + 2;
addOnce(missingLibs, l.substr(a, l.find_first_of(": \n\r", a) - a));
} else if ((a = l.find("ld: library '")) != std::string::npos) { // Apple ld: library 'x' not found
a += 13;
addOnce(missingLibs, l.substr(a, l.find('\'', a) - a));
} else if ((a = l.find("framework '")) != std::string::npos && l.find("not found") != std::string::npos) {
a += 11;
addOnce(missingLibs, l.substr(a, l.find('\'', a) - a) + ".framework");
} else if (l.rfind(" \"", 0) == 0 && (a = l.find("\", referenced from:")) != std::string::npos) {
// Apple ld: Undefined symbols for architecture arm64:\n "_name", referenced from:
std::string name = l.substr(3, a - 3);
if (g_target.darwin && !name.empty() && name[0] == '_') name.erase(0, 1);
addOnce(missing, name);
}
}
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());
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 bool endsWith(const std::string &s, const char *x) {
size_t n = std::strlen(x);
return s.size() > n && s.compare(s.size() - n, n, x) == 0;
}
static void link(const std::string &obj, const std::string &exe, const std::vector<std::string> &libs,
const CImports &c) {
std::string cmd = g_target.cc + " " + shellQuote(obj) + " " + shellQuote(runtimeObject()) + " -o " + shellQuote(exe);
if (!g_target.msvc) cmd += " -lm";
std::vector<std::string> temps;
for (const std::string &l : libs) {
if (endsWith(l, ".c")) { // compile the user's C file
std::string o = obj + "." + std::to_string(temps.size()) + ".o", output;
std::string pic = g_target.windows ? "" : " -fPIC";
if (capture(g_target.cc + " -O2" + pic + " -c " + shellQuote(l) + " -o " + shellQuote(o), output) != 0) {
std::fprintf(stderr, "%serror: couldn't compile %s\n", output.c_str(), l.c_str());
std::exit(1);
}
temps.push_back(o);
cmd += " " + shellQuote(o);
} else if (endsWith(l, ".o") || endsWith(l, ".a")) {
cmd += " " + shellQuote(l);
} else {
cmd += libFlags(l);
}
}
std::string output;
int status = capture(cmd, output);
std::remove(obj.c_str());
for (const std::string &t : temps) std::remove(t.c_str());
if (status == 127) {
std::fprintf(stderr, "error: no C compiler found to link with ('%s'; install one, or set FINCH_CC)\n", g_target.cc.c_str());
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 runLanguageServer(); // lsp.cpp
int main(int argc, char **argv) {
if (argc == 2 && std::string(argv[1]) == "lsp") return runLanguageServer();
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];
if (cmd != "run" && cmd != "build" && cmd != "ir") usage();
std::string out = stem(file);
std::vector<std::string> libs, programArgs;
std::string targetName;
bool optimizeCode = true, debug = false, outGiven = false;
for (int i = 3; i < argc; i++) {
std::string a = argv[i];
if (cmd == "run" && !programArgs.empty()) programArgs.push_back(a);
else if (a == "-o" && i + 1 < argc && cmd == "build") out = argv[++i], outGiven = true;
else if (a == "--target" && i + 1 < argc) targetName = argv[++i];
else if (a == "-O0") optimizeCode = false;
else if (a == "-g") debug = true;
else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]);
else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2));
else if (cmd == "run") programArgs.push_back(a);
else usage();
}
setTarget(targetName);
if (!outGiven) out += g_target.exe;
Loader loader;
loader.load(file, "", Pos{});
std::vector<Import> allImports;
std::vector<std::string> dirs;
for (const Program &p : loader.progs) {
allImports.insert(allImports.end(), p.imports.begin(), p.imports.end());
for (const Link &l : p.links) {
bool file = endsWith(l.lib, ".c") || endsWith(l.lib, ".o") || endsWith(l.lib, ".a");
std::string path = file && !sys::path::is_absolute(l.lib) ? dirOf(p.path) + "/" + l.lib : l.lib;
if (file && !sys::fs::exists(path)) failAt(l.pos.file, l.pos.line, l.pos.col, "can't find the file '" + path + "'");
libs.push_back(path);
}
if (std::find(dirs.begin(), dirs.end(), dirOf(p.path)) == dirs.end()) dirs.push_back(dirOf(p.path));
}
CImports c = importHeaders(allImports, dirs);
LLVMContext ctx;
TargetMachine *tm = targetMachine();
std::unique_ptr<Module> mod = generate(loader.progs, c, ctx, *tm, debug);
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", g_target.windows ? "exe" : "", tmp);
out = tmp.str().str();
obj = out + ".o";
}
emitObject(*mod, tm, obj);
link(obj, out, libs, c);
if (cmd == "run") {
std::string line = shellQuote(out);
for (const std::string &a : programArgs) line += " " + shellQuote(a);
line = runPrefix() + line; // wine / qemu for programs built for another system
#ifdef _WIN32
line = "\"" + line + "\""; // cmd /c strips one pair of outer quotes
#endif
std::string crash;
int status = runCommand(line, &crash);
std::remove(out.c_str());
if (!crash.empty()) std::fprintf(stderr, "\nthe program crashed: %s\n", crash.c_str());
return status;
}
return 0;
}

View file

@ -11,6 +11,7 @@ import parser
import gen import gen
import cimport import cimport
import platform import platform
import linker
import "stdlib.h" import "stdlib.h"
fn dirOf(str path) -> str { fn dirOf(str path) -> str {
@ -100,20 +101,6 @@ fn load(ptr[Loader] l, str path, str module) {
} }
} }
// -lname, or whatever pkg-config says the library needs
fn libFlags(str name) -> str {
tmp := "/tmp/finch_pkg_flags.txt"
code := shell("pkg-config --libs '" + name + "' > '" + tmp + "' 2> /dev/null")
if code == 0 && file_exists(tmp) {
flags := read_file(tmp).trim()
shell("rm -f '" + tmp + "'")
if flags.len > 0 {
return flags
}
}
return "-l" + name
}
fn usage() { fn usage() {
print("Finch -- the compiler is written in Finch") print("Finch -- the compiler is written in Finch")
print("usage:") print("usage:")
@ -131,10 +118,6 @@ fn usage() {
exit(1) exit(1)
} }
fn quote(str a) -> str {
return "'" + a.replace("'", "'\\''") + "'"
}
fn main([]str args) -> int { fn main([]str args) -> int {
if args.len == 2 && (args[1] == "version" || args[1] == "--version") { if args.len == 2 && (args[1] == "version" || args[1] == "--version") {
print("Finch 2.4.0 (the compiler is written in Finch)") print("Finch 2.4.0 (the compiler is written in Finch)")
@ -171,10 +154,10 @@ fn main([]str args) -> int {
} else if a == "-O0" { } else if a == "-O0" {
opt = "-O0" opt = "-O0"
} else if a == "-l" && ai + 1 < args.len { } else if a == "-l" && ai + 1 < args.len {
extraLibs.push(libFlags(args[ai + 1])) extraLibs.push(args[ai + 1])
ai += 1 ai += 1
} else if a.starts_with("-l") && a.len > 2 { } else if a.starts_with("-l") && a.len > 2 {
extraLibs.push(libFlags(a.sub(2, a.len))) extraLibs.push(a.sub(2, a.len))
} else if a == "-I" && ai + 1 < args.len { } else if a == "-I" && ai + 1 < args.len {
extraDirs.push(args[ai + 1]) extraDirs.push(args[ai + 1])
ai += 1 ai += 1
@ -228,7 +211,7 @@ fn main([]str args) -> int {
[]cimport.Header headers []cimport.Header headers
[]str seen []str seen
[]str dirs = extraDirs []str dirs = extraDirs
[]str libs = extraLibs []str links = extraLibs
for prog in loader.progs { for prog in loader.progs {
d := dirOf(prog.path) d := dirOf(prog.path)
if !dirs.contains(d) { if !dirs.contains(d) {
@ -241,10 +224,10 @@ fn main([]str args) -> int {
} }
} }
for l in prog.links { for l in prog.links {
if l.ends_with(".c") { if l.ends_with(".c") || l.ends_with(".o") || l.ends_with(".a") {
libs.push("'" + d + "/" + l + "'") links.push(d + "/" + l) // the program's own C code, next to the .fch file
} else { } else {
libs.push(libFlags(l)) links.push(l)
} }
} }
} }
@ -277,13 +260,6 @@ fn main([]str args) -> int {
print("error: can't find the Finch runtime (runtime/finch_rt.c)") print("error: can't find the Finch runtime (runtime/finch_rt.c)")
return 1 return 1
} }
llPath := out + ".ll"
write_file(llPath, ir)
// the IR is always compiled by clang (CC may be gcc, which can't read it); FINCH_CC picks the linker for another system
clang := getenv("FINCH_CC")
if clang.len == 0 || tg.cross {
clang = "clang"
}
extra := "" extra := ""
if shared { if shared {
extra = " -fPIC" extra = " -fPIC"
@ -291,37 +267,16 @@ fn main([]str args) -> int {
if debug { if debug {
extra = extra + " -g" // or clang drops the debug info that is in the IR extra = extra + " -g" // or clang drops the debug info that is in the IR
} }
code := 0 if linker.build(linker.Build(out: out, ir: ir, rt: rt, opt: opt, extra: extra, shared: shared, links: links), tg, ci) != 0 {
if tg.cross {
// an object for the target, then the target's own C compiler links it with the runtime
obj := out + ".o"
code = shell("clang --target=" + tg.triple + " " + opt + extra + " -w -c '" + llPath + "' -o '" + obj + "'")
if code == 0 {
linkShared := ""
if shared {
linkShared = " -shared"
}
code = shell(tg.cc + " '" + obj + "' '" + rt + "' -o '" + out + "' -lm" + linkShared + " " + libs.join(" "))
}
shell("rm -f '" + obj + "'")
} else {
if shared {
extra = extra + " -shared"
}
code = shell(clang + " " + opt + extra + " -w '" + llPath + "' '" + rt + "' -o '" + out + "' -lm " + libs.join(" "))
}
shell("rm -f '" + llPath + "'")
if code != 0 {
print("error: clang failed")
return 1 return 1
} }
if cmd == "run" { if cmd == "run" {
[]str quoted []str quoted
for a in progArgs { for a in progArgs {
quoted.push(quote(a)) quoted.push(linker.quote(a))
} }
status := shell(tg.runner + quote(out) + " " + quoted.join(" ")) status := shell(tg.runner + linker.quote(out) + " " + quoted.join(" "))
shell("rm -f " + quote(out)) shell("rm -f " + linker.quote(out))
return status return status
} }
return 0 return 0

View file

@ -1,602 +0,0 @@
#include "parser.h"
#include "error.h"
#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <cstring>
#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:
Parser(const std::vector<Token> &t, int f) : toks(t), file(f) {}
Program program() {
Program p;
while (!at(Tok::End)) {
switch (cur().kind) {
case Tok::Import: p.imports.push_back(import()); break;
case Tok::Link: p.links.push_back(link()); break;
case Tok::Struct: p.structs.push_back(structDecl()); break;
default: p.fns.push_back(function()); break;
}
}
return p;
}
private:
const std::vector<Token> &toks;
int file;
size_t i = 0;
int parenDepth = 0; // inside ( ) and [ ] newlines don't end anything
const Token &cur() const { return toks[i]; }
const Token &peekTok(size_t off = 1) const { return toks[std::min(i + off, toks.size() - 1)]; }
const Token &next() { return toks[i++]; }
bool at(Tok k) const { return cur().kind == k; }
Pos pos() const { return {cur().line, cur().col, file}; }
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 brackets).
bool sameLine() const { return parenDepth > 0 || !cur().newlineBefore; }
// int, u8, ptr, ptr[int], []str, map[str]int, Point, math.Vec ...
Type type() {
if (accept(Tok::LBracket)) {
expect(Tok::RBracket, "']' (an array type looks like []int)");
return Type::arrayOf(type());
}
if (!at(Tok::Ident)) unexpected("(expected a type like int, str, []int, ptr[int] or a struct name)");
if (cur().text == "map" && peekTok().kind == Tok::LBracket) {
next();
next();
Type k = type();
expect(Tok::RBracket, "']' (a map type looks like map[str]int)");
return Type::mapOf(k, type());
}
std::string name = next().text;
Type t;
if (typeFromName(name, t)) {
if (t.kind == Type::Ptr && accept(Tok::LBracket)) {
t = Type::ptrTo(type());
expect(Tok::RBracket);
}
return t;
}
t = Type(Type::Named);
t.name = name;
if (at(Tok::Dot) && peekTok().kind == Tok::Ident) { // module.Struct
next();
t.module = name;
t.name = next().text;
}
return t;
}
// Does a declaration like `int x`, `ptr[int] p`, `[]int a`, `Point p` or `math.Vec v` start here?
bool atDeclaration() const {
if (at(Tok::LBracket)) return peekTok().kind == Tok::RBracket;
if (!at(Tok::Ident)) return false;
Tok n = peekTok().kind;
if (n == Tok::Ident) return !peekTok().newlineBefore;
if ((cur().text == "ptr" || cur().text == "map") && n == Tok::LBracket) return true;
return n == Tok::Dot && peekTok(2).kind == Tok::Ident && peekTok(3).kind == Tok::Ident &&
!peekTok(3).newlineBefore;
}
// ---------- top level ----------
Import import() {
Import im;
im.pos = pos();
expect(Tok::Import);
if (at(Tok::Ident)) {
im.isC = false;
im.path = next().text;
} else {
im.path = expect(Tok::String, "a module name (import math) or a C header in quotes (import \"stdio.h\")").text;
}
return im;
}
Link link() {
Link l;
l.pos = pos();
expect(Tok::Link);
l.lib = expect(Tok::String, "a library name in quotes, like link \"glfw\"").text;
auto ends = [&](const char *x) { size_t n = std::strlen(x); return l.lib.size() > n && l.lib.compare(l.lib.size() - n, n, x) == 0; };
if (ends(".c") || ends(".o") || ends(".a")) return l; // your own C code, next to the .fch file
if (l.lib.rfind("lib", 0) == 0 || l.lib.find(".so") != std::string::npos || l.lib.find('/') != std::string::npos)
fail(l.pos.line, l.pos.col, "write just the library's name: for libglfw.so that's link \"glfw\"");
return l;
}
StructDecl structDecl() {
StructDecl s;
s.pos = pos();
expect(Tok::Struct);
s.namePos = pos();
s.name = expect(Tok::Ident, "a struct name").text;
if (isTypeName(s.name)) fail(s.pos.line, s.pos.col, "'" + s.name + "' is a built-in type name, pick another");
Pos open = pos();
expect(Tok::LBrace);
while (!at(Tok::RBrace)) {
if (at(Tok::End)) fail(open.line, open.col, "this '{' is never closed (missing '}')");
Field f;
f.pos = pos();
if (!atDeclaration()) unexpected("(a field looks like: int x or str name = \"default\")");
f.type = type();
f.namePos = pos();
f.name = expect(Tok::Ident, "a field name").text;
for (const Field &other : s.fields)
if (other.name == f.name) fail(f.pos.line, f.pos.col, "the field '" + f.name + "' is listed twice");
if (accept(Tok::Assign)) f.init = expr();
s.fields.push_back(std::move(f));
endOfStatement();
}
next();
return s;
}
FnDecl function() {
FnDecl fn;
fn.pos = pos();
if (at(Tok::Ident) && isTypeName(cur().text))
fail(fn.pos.line, fn.pos.col, "functions start with 'fn', like: fn add(int a, int b) -> int { ... }");
expect(Tok::Fn, "'fn', 'struct', 'import' or 'link' (every function starts with fn)");
fn.namePos = pos();
fn.name = expect(Tok::Ident, "a function name").text;
if (accept(Tok::Dot)) { // fn Point.move(...): a method
fn.recv = fn.name;
fn.recvPos = fn.namePos;
fn.namePos = pos();
fn.name = expect(Tok::Ident, "a method name (fn Point.name(...))").text;
}
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do {
Param p;
p.pos = pos();
p.type = type();
p.namePos = pos();
p.name = expect(Tok::Ident, "a parameter name").text;
fn.params.push_back(p);
} while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
fn.ret = Type(Type::Void);
if (accept(Tok::Arrow)) {
if (!at(Tok::Not)) fn.ret = type();
fn.fallible = accept(Tok::Not); // -> int! or -> !
if (!fn.fallible && fn.ret.kind == Type::Void) unexpected("(after '->' comes a type, or ! for a function that can fail)");
}
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;
b->end = pos();
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();
Pos varPos = pos();
std::string var = expect(Tok::Ident, "a loop variable name").text;
std::string var2;
Pos var2Pos;
if (accept(Tok::Comma)) { // for i, x in list / for key, value in map
var2Pos = pos();
var2 = expect(Tok::Ident, "a second loop variable name").text;
}
expect(Tok::In);
ExprPtr first = expr();
if (!var2.empty() && at(Tok::DotDot)) fail(var2Pos.line, var2Pos.col, "a range (for i in 0..n) has one loop variable");
if (accept(Tok::DotDot)) { // for i in 0..10
auto s = std::make_unique<ForStmt>(p);
s->var = var;
s->varPos = varPos;
s->from = std::move(first);
s->to = expr();
s->body = block();
return s;
}
auto s = std::make_unique<ForEachStmt>(p); // for x in list
s->var = var;
s->varPos = varPos;
s->var2 = var2;
s->var2Pos = var2Pos;
s->list = std::move(first);
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::Defer: {
next();
if (at(Tok::Defer)) fail(p.line, p.col, "defer inside defer is not allowed");
return std::make_unique<DeferStmt>(p, statement());
}
case Tok::LBrace: return block();
case Tok::Fn: fail(p.line, p.col, "functions can't be inside other functions (move it out)");
case Tok::Struct: fail(p.line, p.col, "structs go outside functions, at the top level of the file");
case Tok::Import:
case Tok::Link: fail(p.line, p.col, "import and link go at the top of the file, outside functions");
default: break;
}
// int x = 5, Point p, []int a
if (atDeclaration()) {
auto s = std::make_unique<VarDeclStmt>(p);
s->hasType = true;
s->type = type();
s->namePos = pos();
s->name = expect(Tok::Ident, "a variable name").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->namePos = pos();
s->name = next().text;
next();
s->init = expr();
return s;
}
ExprPtr e = expr();
// x = 5, p.value += 1, a[i] = 2, ...
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 && e->kind != ExprKind::Index)
fail(p.line, p.col, "only a variable, a field, an element or p.value can be assigned to");
auto s = std::make_unique<AssignStmt>(p);
s->target = std::move(e);
s->op = op;
next();
s->value = expr();
return s;
}
if (e->kind != ExprKind::Call && e->kind != ExprKind::Method && e->kind != ExprKind::Try && e->kind != ExprKind::OrElse)
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) ----------
// f(x) or fallback / f(x) or { ... }: the lowest precedence, right to left
ExprPtr expr() {
ExprPtr l = orExpr();
if (!sameLine() || !at(Tok::OrElse)) return l;
Pos p = pos();
next();
auto e = std::make_unique<OrElseExpr>(p, std::move(l));
if (at(Tok::LBrace)) e->block = block();
else e->fallback = expr();
return e;
}
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());
if (accept(Tok::Try)) return std::make_unique<TryExpr>(p, postfix());
return postfix();
}
// p.value, a.len, a[i], a.push(x), math.sqrt(2)
ExprPtr postfix() {
ExprPtr e = primary();
while (sameLine()) {
Pos p = pos();
if (accept(Tok::Dot)) {
std::string name = expect(Tok::Ident, "a name after '.'").text;
if (at(Tok::LParen) && !cur().newlineBefore) {
auto m = std::make_unique<MethodExpr>(p, std::move(e), name);
args(m->args, m->argNames);
e = std::move(m);
} else {
e = std::make_unique<MemberExpr>(p, std::move(e), name);
}
} else if (at(Tok::LBracket)) {
next();
parenDepth++;
ExprPtr index = expr();
parenDepth--;
expect(Tok::RBracket);
e = std::make_unique<IndexExpr>(p, std::move(e), std::move(index));
} else {
return e;
}
}
return e;
}
// ( arg, name: arg, ... ) -- we are at the '('
void args(std::vector<ExprPtr> &out, std::vector<std::string> &names) {
expect(Tok::LParen);
parenDepth++;
if (!at(Tok::RParen)) {
do {
if (at(Tok::RParen)) break; // a trailing comma is fine
std::string name;
if (at(Tok::Ident) && peekTok().kind == Tok::Colon) {
name = next().text;
next();
}
names.push_back(name);
out.push_back(expr());
} while (accept(Tok::Comma));
}
parenDepth--;
expect(Tok::RParen);
}
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::LBracket: { // [1, 2, 3] or a map: ["a": 1, "b": 2], [:]
next();
parenDepth++;
if (accept(Tok::Colon)) {
parenDepth--;
expect(Tok::RBracket, "']' (an empty map is [:])");
return std::make_unique<MapLitExpr>(p);
}
if (!at(Tok::RBracket)) {
ExprPtr first = expr();
if (accept(Tok::Colon)) {
auto m = std::make_unique<MapLitExpr>(p);
m->keys.push_back(std::move(first));
m->values.push_back(expr());
while (accept(Tok::Comma) && !at(Tok::RBracket)) { // a trailing comma is fine
m->keys.push_back(expr());
expect(Tok::Colon, "':' (every map entry is key: value)");
m->values.push_back(expr());
}
parenDepth--;
expect(Tok::RBracket);
return m;
}
auto a = std::make_unique<ArrayLitExpr>(p);
a->elems.push_back(std::move(first));
while (accept(Tok::Comma) && !at(Tok::RBracket)) a->elems.push_back(expr());
parenDepth--;
expect(Tok::RBracket);
return a;
}
auto a = std::make_unique<ArrayLitExpr>(p);
parenDepth--;
expect(Tok::RBracket);
return a;
}
case Tok::Ident: {
next();
if (at(Tok::LParen) && !cur().newlineBefore) {
auto call = std::make_unique<CallExpr>(p, t.text);
args(call->args, call->argNames);
return call;
}
if (isTypeName(t.text))
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, int file) {
return Parser(tokens, file).program();
}

View file

@ -239,8 +239,13 @@ fn parseProgram(ptr[Parser] p) -> ast.Program {
prog.imports.push(next(p).text) prog.imports.push(next(p).text)
} }
} else if at(p, "link") { } else if at(p, "link") {
next(p) lt := next(p)
prog.links.push(expect(p, "string", "a library or C file name, like link \"glfw\"").text) lib := expect(p, "string", "a library name in quotes, like link \"glfw\"").text
ownCode := lib.ends_with(".c") || lib.ends_with(".o") || lib.ends_with(".a") // your own C code, next to the .fch file
if !ownCode && (lib.starts_with("lib") || lib.contains(".so") || lib.contains("/")) {
fail(p, lt, "write just the library's name: for libglfw.so that's link \"glfw\"")
}
prog.links.push(lib)
} else if at(p, "struct") { } else if at(p, "struct") {
prog.structs.push(parseStruct(p)) prog.structs.push(parseStruct(p))
} else if at(p, "enum") { } else if at(p, "enum") {
@ -377,6 +382,9 @@ fn parseStruct(ptr[Parser] p) -> ast.Node {
fn parseFn(ptr[Parser] p) -> ast.Node { fn parseFn(ptr[Parser] p) -> ast.Node {
f := node(p, "fn") f := node(p, "fn")
f.exported = accept(p, "export") f.exported = accept(p, "export")
if at(p, "ident") && isTypeName(cur(p).text) {
fail(p, cur(p), "functions start with 'fn', like: fn add(int a, int b) -> int { ... }")
}
expect(p, "fn", "'fn', 'struct' or 'import'") expect(p, "fn", "'fn', 'struct' or 'import'")
f.name = expect(p, "ident", "a function name").text f.name = expect(p, "ident", "a function name").text
if at(p, "[") { if at(p, "[") {

View file

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

View file

@ -1,158 +0,0 @@
#include "target.h"
#include <llvm/Support/TargetSelect.h>
#include <llvm/TargetParser/Host.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#else
#include <sys/wait.h>
#endif
static bool hostIsWindows() {
#ifdef _WIN32
return true;
#else
return false;
#endif
}
// The Finch-generated code needs these LLVM backends (the same C functions LLVM's own tools call).
extern "C" {
#ifdef FINCH_TARGET_X86
void LLVMInitializeX86TargetInfo();
void LLVMInitializeX86Target();
void LLVMInitializeX86TargetMC();
void LLVMInitializeX86AsmPrinter();
#endif
#ifdef FINCH_TARGET_AARCH64
void LLVMInitializeAArch64TargetInfo();
void LLVMInitializeAArch64Target();
void LLVMInitializeAArch64TargetMC();
void LLVMInitializeAArch64AsmPrinter();
#endif
}
void initTargets() {
#ifdef FINCH_TARGET_X86
LLVMInitializeX86TargetInfo();
LLVMInitializeX86Target();
LLVMInitializeX86TargetMC();
LLVMInitializeX86AsmPrinter();
#endif
#ifdef FINCH_TARGET_AARCH64
LLVMInitializeAArch64TargetInfo();
LLVMInitializeAArch64Target();
LLVMInitializeAArch64TargetMC();
LLVMInitializeAArch64AsmPrinter();
#endif
llvm::InitializeNativeTarget();
llvm::InitializeNativeTargetAsmPrinter();
}
void setTarget(const std::string &name) {
std::string host = llvm::sys::getDefaultTargetTriple();
llvm::Triple hostTriple(llvm::Triple::normalize(host));
std::string t = name;
if (name.empty()) t = host;
else if (name == "windows") t = hostIsWindows() ? host : "x86_64-w64-mingw32";
else if (name == "linux") t = "x86_64-unknown-linux-gnu";
else if (name == "arm64" || name == "aarch64" || name == "linux-arm64") t = "aarch64-unknown-linux-gnu";
else if (name == "macos") t = hostTriple.isAArch64() ? "arm64-apple-macosx14.0.0" : "x86_64-apple-macosx14.0.0";
g_target.triple = llvm::Triple(llvm::Triple::normalize(t));
g_target.windows = g_target.triple.isOSWindows();
g_target.msvc = g_target.triple.isWindowsMSVCEnvironment();
g_target.darwin = g_target.triple.isOSDarwin();
g_target.cross = hostTriple.getOS() != g_target.triple.getOS() || hostTriple.getArch() != g_target.triple.getArch();
g_target.exe = g_target.windows ? ".exe" : "";
// FINCH_CC chooses the C compiler for any target; CC only when not cross-compiling.
if (const char *fc = std::getenv("FINCH_CC")) g_target.cc = fc;
else if (!g_target.cross && std::getenv("CC")) g_target.cc = std::getenv("CC");
else if (g_target.cross && g_target.windows) g_target.cc = "x86_64-w64-mingw32-gcc";
else if (g_target.cross && g_target.triple.isAArch64() && g_target.triple.isOSLinux()) g_target.cc = "aarch64-linux-gnu-gcc";
else if (g_target.cross) g_target.cc = "clang --target=" + g_target.triple.str();
else if (hostIsWindows()) g_target.cc = "clang";
else g_target.cc = "cc";
}
std::string runPrefix() {
if (!g_target.cross) return "";
if (g_target.windows && !hostIsWindows()) return "wine ";
if (g_target.triple.isAArch64() && g_target.triple.isOSLinux()) return "qemu-aarch64 ";
return "";
}
std::string shellQuote(const std::string &s) {
#ifdef _WIN32
// cmd.exe: double quotes; a " inside a file name is impossible on Windows
return "\"" + s + "\"";
#else
std::string out = "'";
for (char ch : s) out += ch == '\'' ? std::string("'\\''") : std::string(1, ch);
return out + "'";
#endif
}
static int exitStatus(int status) {
#ifdef _WIN32
return status;
#else
if (status == -1) return -1;
if (WIFEXITED(status)) return WEXITSTATUS(status);
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
return -1;
#endif
}
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);
return exitStatus(pclose(p));
}
int runCommand(const std::string &cmd, std::string *crash) {
std::fflush(stdout);
int status = std::system(cmd.c_str());
#ifndef _WIN32
if (crash && status != -1 && WIFSIGNALED(status)) *crash = strsignal(WTERMSIG(status));
#else
// a crashed Windows program exits with an NTSTATUS code like 0xC0000005
if (crash && (unsigned)status >= 0xC0000000u) {
char buf[64];
std::snprintf(buf, sizeof buf, "exception 0x%08X%s", (unsigned)status,
(unsigned)status == 0xC0000005u ? " (access violation)" : (unsigned)status == 0xC00000FDu ? " (stack overflow)" : "");
*crash = buf;
}
#endif
return exitStatus(status);
}
const char *nullDevice() { return hostIsWindows() ? "NUL" : "/dev/null"; }
std::string cacheDir() {
#ifdef _WIN32
if (const char *l = std::getenv("LOCALAPPDATA")) return std::string(l) + "\\finch";
if (const char *t = std::getenv("TEMP")) return std::string(t) + "\\finch";
return "finch-cache";
#else
if (const char *x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/finch";
if (const char *h = std::getenv("HOME")) return std::string(h) + "/.cache/finch";
return "/tmp/finch-cache";
#endif
}
char pathListSeparator() { return hostIsWindows() ? ';' : ':'; }
std::string dirOf(const std::string &path) {
size_t slash = hostIsWindows() ? path.find_last_of("/\\") : path.find_last_of('/');
return slash == std::string::npos ? "." : path.substr(0, slash);
}

View file

@ -1,49 +0,0 @@
#pragma once
// The system Finch compiles for, and everything that depends on it:
// the C compiler used to link, how the shell quotes, file names, the C ABI.
#include <llvm/TargetParser/Triple.h>
#include <string>
struct TargetInfo {
llvm::Triple triple;
bool darwin = false; // macOS
bool windows = false; // Windows (MSVC or MinGW): Microsoft x64 calling convention, .exe
bool msvc = false; // the MSVC environment (link with clang + the Windows SDK)
bool cross = false; // compiling for another system than the one we run on
std::string cc; // the C compiler that links programs and builds the runtime
std::string exe; // ".exe" or ""
};
inline TargetInfo g_target;
// "windows", "linux", "macos", "arm64" (ARM64 Linux), or a full LLVM triple; "" = the system finch runs on
void setTarget(const std::string &name);
// Register the LLVM backends Finch can generate code for (x86-64, ARM64).
void initTargets();
// How to run a program built for g_target on this machine ("" = directly, "wine", "qemu-aarch64").
std::string runPrefix();
// Quote one argument for the shell that runs our commands (sh on Unix, cmd.exe on Windows).
std::string shellQuote(const std::string &s);
// Run a command; its exit status, and everything it printed in `output`.
int capture(const std::string &cmd, std::string &output);
// Run a command with its output going to the terminal; its exit status (128+n for a signal).
int runCommand(const std::string &cmd, std::string *crash = nullptr);
// The null device: /dev/null or NUL.
const char *nullDevice();
// The folder for cached files (~/.cache/finch, %LOCALAPPDATA%\finch).
std::string cacheDir();
// Separator of FINCH_PATH entries (':' or ';').
char pathListSeparator();
// The folder part of a path (both / and \ on Windows).
std::string dirOf(const std::string &path);

View file

@ -1,86 +0,0 @@
#!/usr/bin/env bash
# The self-hosted compiler (boot/) must
# 1. build itself: stage1 (built by build/finch) -> stage2 -> stage3, with identical IR
# 2. give the same output as build/finch on every test in tests/run it supports
set -u
cd "$(dirname "$0")/.."
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
build/finch build boot/main.fch -o "$tmp/boot1" || exit 1
# Stage 2: the compiler built by itself. Until it can read the C headers its own source imports
# (libclang's), this stage can't be made, and the tests below run on stage 1 instead.
boot=$tmp/boot1
if "$tmp/boot1" build boot/main.fch -o "$tmp/boot2" > "$tmp/stage2.err" 2>&1; then
"$tmp/boot1" ir boot/main.fch > "$tmp/stage2.ll"
"$tmp/boot2" ir boot/main.fch > "$tmp/stage3.ll"
if cmp -s "$tmp/stage2.ll" "$tmp/stage3.ll"; then
echo "bootstrap: stage 2 and stage 3 are identical ($(wc -l < "$tmp/stage3.ll") lines of IR)"
boot=$tmp/boot2
else
echo "bootstrap: FAILED, stage 2 and stage 3 differ"
exit 1
fi
else
echo "bootstrap: not yet -- stage 1 can't build itself: $(head -1 "$tmp/stage2.err")"
fi
pass=0; failed=0; skipped=0
# tests/run: what the C++ compiler also runs. tests/new: the language since then (enums, match, ...),
# which only this compiler has.
for f in tests/run/*.fch tests/new/*.fch; do
grep -q "^fn main" "$f" || continue
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
if ! "$boot" build "$f" -o "$tmp/prog" > "$tmp/err" 2>&1; then
failed=$((failed + 1))
echo "FAIL (compile) $f"
head -3 "$tmp/err"
continue
fi
got=$("$tmp/prog" < "$input" 2>&1)
if [ "$got" == "$(cat "${f%.fch}.out")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL (output) $f"
diff <(echo "$got") "${f%.fch}.out" | head -10
fi
done
# programs that must be refused, and say why: the first line is "// expect: <text of the error>"
for f in tests/new/fail/*.fch; do
expect=$(head -1 "$f" | sed -n 's|^// expect: ||p')
got=$("$boot" build "$f" -o "$tmp/refused" 2>&1)
status=$?
if [ $status -ne 0 ] && [[ "$got" == *"$expect"* ]]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL $f (wanted: $expect)"
echo "$got" | head -3
fi
done
# the same programs with debug info (-g -O0): the metadata has to be valid for every kind of thing a program has
for f in tests/new/enums.fch tests/new/generics.fch tests/new/std_json.fch tests/run/basics.fch tests/run/methods_basic.fch \
tests/run/maps.fch tests/run/errors.fch tests/run/defer.fch tests/run/c_structs.fch tests/run/memory.fch; do
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
if "$boot" build "$f" -g -O0 -o "$tmp/progg" > "$tmp/err" 2>&1 && [ "$("$tmp/progg" < "$input" 2>&1)" == "$(cat "${f%.fch}.out")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL (-g) $f"
head -3 "$tmp/err"
fi
done
# a library a C program loads (finch build --shared), with the compiler under test
if FINCH_RT="$PWD/runtime/finch_rt.c" FINCH="$boot" tests/shared.sh > "$tmp/shared.out" 2>&1; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL tests/shared.sh"
head -5 "$tmp/shared.out"
fi
echo "self-hosted compiler: $pass passed, $failed failed, $skipped skipped (features it doesn't have)"
[ $failed -eq 0 ]

View file

@ -1,18 +0,0 @@
import ast
import cimport
fn main() -> ! {
headers := [cimport.Header(path: "stdio.h", where: "test:1:1"), cimport.Header(path: "/home/nexoniarz/.claude/jobs/4af40c6f/tmp/cx/h.h", where: "test:2:1")]
c := try cimport.importHeaders(headers, [])
print("functions", c.fns.len, "consts", c.consts.len, "globals", c.globals.len, "structs", c.structs.len)
f := c.fns["add"]
print(f.name, ast.show(f.ret), f.params.len, f.unsupported)
m := c.fns["many"]
print(m.name, ast.show(m.ret), m.params.len, ast.show(m.params[0]), ast.show(m.params[1]), m.unsupported)
print(c.consts["N"].i, c.consts["Green"].i, c.consts["EOF"].i)
print(ast.show(c.globals["counter"]), ast.show(c.globals["stdout"]))
s := c.structs["Vec2"]
print(s.name, s.size, s.align, s.fields.len, s.unsupported)
printf := c.fns["printf"]
print(printf.name, printf.variadic, printf.params.len, ast.show(printf.params[0]))
}

View file

@ -3,16 +3,19 @@
# tests/run/*.fch must compile and print exactly what is in the matching .out file # tests/run/*.fch must compile and print exactly what is in the matching .out file
# (files without `fn main` are modules or helpers and are skipped; # (files without `fn main` are modules or helpers and are skipped;
# a matching .in file is fed to the program as its input) # a matching .in file is fed to the program as its input)
# tests/fail/*.fch must fail; the first line is "// expect: <text that must appear in the error>" # tests/fail/*.fch and tests/new/fail/*.fch must fail; the first line is "// expect: <text that must appear in the error>"
# the same programs built with -g -O0, and a library loaded by a C program (tests/shared.sh)
# MEMCHECK=1 tests/run.sh also runs every program under valgrind: no leaks, no bad memory access # MEMCHECK=1 tests/run.sh also runs every program under valgrind: no leaks, no bad memory access
set -u set -u
cd "$(dirname "$0")" cd "$(dirname "$0")"
FINCH=${FINCH:-../build/finch} FINCH=${FINCH:-../build/finch}
FINCH="$(cd "$(dirname "$FINCH")" && pwd)/$(basename "$FINCH")" # tests/shared.sh runs from another folder
export FINCH_RT=${FINCH_RT:-$PWD/../runtime/finch_rt.c}
pass=0; failed=0 pass=0; failed=0
tmp=$(mktemp -d) tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"' EXIT
for f in run/*.fch; do for f in run/*.fch new/*.fch; do
grep -q "^fn main" "$f" || continue grep -q "^fn main" "$f" || continue
want="${f%.fch}.out" want="${f%.fch}.out"
input="${f%.fch}.in" input="${f%.fch}.in"
@ -38,7 +41,7 @@ for f in run/*.fch; do
pass=$((pass + 1)) pass=$((pass + 1))
done done
for f in fail/*.fch; do for f in fail/*.fch new/fail/*.fch; do
expect=$(head -1 "$f" | sed -n 's|^// expect: ||p') expect=$(head -1 "$f" | sed -n 's|^// expect: ||p')
got=$("$FINCH" run "$f" < /dev/null 2>&1) got=$("$FINCH" run "$f" < /dev/null 2>&1)
status=$? status=$?
@ -52,5 +55,28 @@ for f in fail/*.fch; do
fi fi
done done
# the same programs with debug info (-g -O0): the metadata has to be valid for every kind of thing a program has
for f in new/enums.fch new/generics.fch new/std_json.fch run/basics.fch run/methods_basic.fch run/maps.fch run/errors.fch \
run/defer.fch run/c_structs.fch run/memory.fch; do
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
if "$FINCH" build "$f" -g -O0 -o "$tmp/progg" > "$tmp/err" 2>&1 && [ "$("$tmp/progg" < "$input" 2>&1)" == "$(cat "${f%.fch}.out")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL (-g) $f"
head -3 "$tmp/err"
fi
done
# a library a C program loads (finch build --shared)
if FINCH="$FINCH" ./shared.sh > "$tmp/shared.out" 2>&1; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL shared.sh"
head -5 "$tmp/shared.out"
fi
echo "$pass passed, $failed failed" echo "$pass passed, $failed failed"
[ $failed -eq 0 ] [ $failed -eq 0 ]

View file

@ -4,6 +4,7 @@
set -e set -e
cd "$(dirname "$0")/shared" cd "$(dirname "$0")/shared"
FINCH=${FINCH:-../../build/finch} FINCH=${FINCH:-../../build/finch}
export FINCH_RT=${FINCH_RT:-$PWD/../../runtime/finch_rt.c}
tmp=$(mktemp -d) tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"' EXIT
"$FINCH" build mod.fch --shared -o "$tmp/libmod.so" "$FINCH" build mod.fch --shared -o "$tmp/libmod.so"