mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
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:
parent
750eca3e85
commit
b9378050ab
39 changed files with 80403 additions and 7251 deletions
|
|
@ -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
49
bootstrap/build.sh
Executable 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
79936
bootstrap/linux-x86_64.ll
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||||
|
|
|
||||||
367
src/abi.cpp
367
src/abi.cpp
|
|
@ -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> ¶ms, 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> ¶ms) {
|
|
||||||
LLVMContext &ctx = cg.ctx;
|
|
||||||
AttributeList al;
|
|
||||||
unsigned idx = 0;
|
|
||||||
if (pl.ret.kind == AbiArg::Memory) {
|
|
||||||
al = al.addParamAttribute(ctx, idx, Attribute::getWithStructRetType(ctx, cg.ty(ret)));
|
|
||||||
idx++;
|
|
||||||
} else if (Attribute::AttrKind k = extAttr(ret); k != Attribute::None) {
|
|
||||||
al = al.addRetAttribute(ctx, k);
|
|
||||||
}
|
|
||||||
for (size_t i = 0; i < params.size(); i++) {
|
|
||||||
const AbiArg &a = pl.params[i];
|
|
||||||
if (a.kind == AbiArg::Memory) {
|
|
||||||
uint64_t align = std::max<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
356
src/ast.h
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
@ -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"};
|
|
||||||
373
src/cimport.cpp
373
src/cimport.cpp
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
1505
src/codegen.cpp
1505
src/codegen.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
48
src/error.h
48
src/error.h
|
|
@ -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); }
|
|
||||||
|
|
@ -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") {
|
||||||
39
src/index.h
39
src/index.h
|
|
@ -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
|
|
||||||
214
src/lexer.cpp
214
src/lexer.cpp
|
|
@ -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 "?";
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
36
src/lexer.h
36
src/lexer.h
|
|
@ -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
304
src/linker.fch
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
20
src/loader.h
20
src/loader.h
|
|
@ -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);
|
|
||||||
580
src/lsp.cpp
580
src/lsp.cpp
|
|
@ -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(); }
|
|
||||||
350
src/main.cpp
350
src/main.cpp
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
602
src/parser.cpp
602
src/parser.cpp
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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, "[") {
|
||||||
|
|
@ -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);
|
|
||||||
158
src/target.cpp
158
src/target.cpp
|
|
@ -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);
|
|
||||||
}
|
|
||||||
49
src/target.h
49
src/target.h
|
|
@ -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);
|
|
||||||
|
|
@ -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 ]
|
|
||||||
|
|
@ -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]))
|
|
||||||
}
|
|
||||||
32
tests/run.sh
32
tests/run.sh
|
|
@ -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 ]
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue