diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66f8c2a..3f2accc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,8 @@ jobs: - name: Build docker image run: docker build -t tlang-dev . - name: Run tests inside container - run: docker run -v $(pwd):/tlang -w /tlang --entrypoint /bin/sh tlang-dev -c ./testing.sh + # tests must not run as root, look at tests/unit/sources/SourceManagerTests.cpp to learn more + run: docker run --user $(id -u):$(id -g) -e HOME=/tmp -v $(pwd):/tlang -w /tlang --entrypoint /bin/sh tlang-dev -c ./testing.sh - name: Unit Test Report uses: dorny/test-reporter@v3 if: always() diff --git a/.gitignore b/.gitignore index 25417fb..0655561 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ bin files/grammar/grammar.lr files/lexis/lexis.lx src/syntax/BuildersRegistry.h +files/std/lib +files/std/include examples/build examples/exe diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d0acc9..82828a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory(src/lexis) add_subdirectory(src/syntax) add_subdirectory(src) +add_subdirectory(std) # add executables from cli directory file(GLOB_RECURSE CLI_SOURCES diff --git a/cmake/load_libraries.cmake b/cmake/load_libraries.cmake index 23ba1eb..6a39f34 100644 --- a/cmake/load_libraries.cmake +++ b/cmake/load_libraries.cmake @@ -22,6 +22,7 @@ llvm_map_components_to_libnames(llvm_libs support core irreader linker # -- fmt -- set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) +set(FMT_INSTALL OFF) add_subdirectory(lib/fmt EXCLUDE_FROM_ALL) # -- fmt end -- @@ -30,5 +31,6 @@ add_subdirectory(lib/argparse EXCLUDE_FROM_ALL) # -- argparse end -- # -- google test -- +set(INSTALL_GTEST OFF) add_subdirectory(lib/googletest EXCLUDE_FROM_ALL) # -- google test end -- \ No newline at end of file diff --git a/examples/Makefile b/examples/Makefile index 095c37c..040268f 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,19 +1,9 @@ -build/print.s : sources/print.cpp +build/main : sources/main.tea sources/rectangle.team sources/math.team mkdir -p build - clang++ sources/print.cpp -S -o build/print.s + tlang main:sources/main.tea rectangle:sources/rectangle.team math:sources/math.team -o build/main -build/main.s : sources/main.tea sources/io.team sources/rectangle.team sources/math.team - mkdir -p build - tlang main:sources/main.tea io:sources/io.team rectangle:sources/rectangle.team math:sources/math.team -o build/main.ll - /opt/homebrew/Cellar/llvm/22.1.7_1/bin/llc build/main.ll -o build/main.s - -exe/main: build/main.s build/print.s - mkdir -p exe - clang++ build/main.s build/print.s -o exe/main - -run_code: exe/main - ./exe/main +run_code: build/main + ./build/main clean: - rm -rf exe rm -rf build \ No newline at end of file diff --git a/examples/sources/io.team b/examples/sources/io.team deleted file mode 100644 index 4a287f7..0000000 --- a/examples/sources/io.team +++ /dev/null @@ -1 +0,0 @@ -export extern print: (value: i64) -> () \ No newline at end of file diff --git a/examples/sources/main.tea b/examples/sources/main.tea index 8dd1d51..e131813 100644 --- a/examples/sources/main.tea +++ b/examples/sources/main.tea @@ -1,9 +1,10 @@ -import "rectangle" import "io" + import "math" +import "rectangle" main: () -> i64 = { - print(math::factorial(6)); + println(math::factorial(6)); rect: geometry::Rectangle = geometry::make_rectangle(1 as! u64, 2 as! u64, 3, 4); @@ -11,5 +12,9 @@ main: () -> i64 = { .geometry::move(1, 2) .geometry::print_rect(); + println(1); + println(2); + println(3); + return 0; } \ No newline at end of file diff --git a/examples/sources/print.cpp b/examples/sources/print.cpp deleted file mode 100644 index d509484..0000000 --- a/examples/sources/print.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include - -struct Rectangle { - int64_t x; - int64_t y; - int64_t width; - int64_t height; - - Rectangle(int64_t x, int64_t y, int64_t width, int64_t height): - x(x), - y(y), - width(width), - height(height) {} -}; - -void print(long long value) { - std::cout << value << std::endl; -} - -Rectangle get_rectangle() { - return Rectangle(1, 2, 3, 4); -} - diff --git a/files/std/reclib.asm b/files/std/reclib.asm deleted file mode 100644 index e31d788..0000000 --- a/files/std/reclib.asm +++ /dev/null @@ -1,19 +0,0 @@ -.p2align 4 -.global _print -_print: -stp x29, x30, [sp, #-16]! -mov x29, sp - -sub sp, sp, #16 -str x0, [sp] -adrp x0, format@PAGE -add x0, x0, format@PAGEOFF -mov x1, sp -bl _printf -add sp, sp, #16 - -ldp x29, x30, [sp], #16 -ret - -.data -format: .asciz "%i\n" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cf86c59..c6f84f8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,3 +35,6 @@ target_include_directories(TeaLang PUBLIC ${LLVM_INCLUDE_DIRS}) # TeaLang requires grammar and lexis tables to work add_dependencies(TeaLang run_grammar_tablegen run_lexis_tablegen) + +# TeaLang comes with a standard library +add_dependencies(TeaLang std std_headers) diff --git a/src/Constants.h b/src/Constants.h index 998183e..e7cc615 100644 --- a/src/Constants.h +++ b/src/Constants.h @@ -16,6 +16,8 @@ extern const bool is_installed_build; constexpr inline auto lexis_relative_filepath = "lexis/lexis.lx"; constexpr inline auto grammar_relative_filepath = "grammar/grammar.lr"; +constexpr inline auto std_library_relative_filepath = "std/lib/libstd.a"; +constexpr inline auto std_include_relative_path = "std/include"; inline std::filesystem::path GetRuntimeFilePath( std::filesystem::path relative_path) { diff --git a/src/cli/ArgumentsReader.cpp b/src/cli/ArgumentsReader.cpp index f608473..57bdf95 100644 --- a/src/cli/ArgumentsReader.cpp +++ b/src/cli/ArgumentsReader.cpp @@ -12,17 +12,23 @@ namespace fs = std::filesystem; namespace Cli { -SourcesList ArgumentsReader::parse_source_paths( - std::vector sources) { - SourcesList result; - auto add_source = [&result](const std::string& name, const fs::path& path) { - auto [_, was_emplaced] = result.emplace(name, path); - if (!was_emplaced) { - throw std::runtime_error(fmt::format( - "File {} was already included with different name.", path.c_str())); - } - }; +std::string ArgumentsReader::get_default_output_name(Front::EmitType type) { + switch (type) { + case Front::EmitType::AST: + case Front::EmitType::IR: + return "out.txt"; + case Front::EmitType::OBJECT: + return "out.o"; + case Front::EmitType::EXECUTABLE: + return "out"; + } + + unreachable("All output types should be enumerated above."); +} +void ArgumentsReader::parse_source_paths( + const std::vector& sources, + Front::TeaFrontendConfiguration& config) { std::string separator{fs::path::preferred_separator}; for (auto& include : sources) { @@ -42,7 +48,7 @@ SourcesList ArgumentsReader::parse_source_paths( "Named include \"{}\" must refer to regular file.", name)); } - add_source(name, path); + config.add_source(name, path); } else { // unnamed include // for this type of include name is a stem part of path @@ -60,7 +66,7 @@ SourcesList ArgumentsReader::parse_source_paths( auto include_name = std::regex_replace(std::string{path_copy}, std::regex(separator), "."); - add_source(include_name, path); + config.add_source(include_name, path); continue; } @@ -85,23 +91,36 @@ SourcesList ArgumentsReader::parse_source_paths( auto include_name = std::regex_replace(relative_path, std::regex(separator), "."); - add_source(include_name, subfile.path()); + config.add_source(include_name, subfile.path()); } } } - - return result; } -std::filesystem::path ArgumentsReader::parse_output(const std::string& output) { +std::filesystem::path ArgumentsReader::parse_output(std::string output, + Front::EmitType emit_type) { + // empty output means that default value is used + if (output.empty()) { + switch (emit_type) { + case Front::EmitType::AST: + case Front::EmitType::IR: + // write to stdout + return {}; + case Front::EmitType::OBJECT: + case Front::EmitType::EXECUTABLE: + output = get_default_output_name(emit_type); + break; + } + } + fs::path output_path = output; // output must be directory or path if (fs::is_directory(output)) { - output_path /= kDefaultOutputName; + output_path /= get_default_output_name(emit_type); } - return output_path; + return fs::absolute(output_path).lexically_normal(); } Front::EmitType ArgumentsReader::get_emit_type(std::string_view name) { @@ -111,8 +130,11 @@ Front::EmitType ArgumentsReader::get_emit_type(std::string_view name) { if (name == "ast") { return Front::EmitType::AST; } - if (name == "binary") { - return Front::EmitType::BINARY; + if (name == "obj") { + return Front::EmitType::OBJECT; + } + if (name == "exe") { + return Front::EmitType::EXECUTABLE; } throw std::runtime_error("unknown compiler emit type."); } @@ -129,14 +151,12 @@ Front::TeaFrontendConfiguration ArgumentsReader::read(int argc, char* argv[]) { "automatically or to include all files in " "directory recursively."); - parser.add_argument("-o", "--output") - .default_value("") - .help("output file (stdout by default)"); + parser.add_argument("-o", "--output").default_value("").help("output file"); parser.add_argument("--emit") - .choices("ir", "ast", "binary") - .default_value("ir") - .help("compiler output type: `ir` or `ast`"); + .choices("ir", "ast", "obj", "exe") + .default_value("exe") + .help("compiler output type: ir, ast, obj, exe"); try { parser.parse_args(argc, argv); @@ -145,10 +165,10 @@ Front::TeaFrontendConfiguration ArgumentsReader::read(int argc, char* argv[]) { } Front::TeaFrontendConfiguration result; - result.sources = - parse_source_paths(parser.get>("sources")); + + parse_source_paths(parser.get>("sources"), result); result.emit_type = get_emit_type(parser.get("emit")); - result.output_file = parse_output(parser.get("output")); + result.output_file = parse_output(parser.get("output"), result.emit_type); return result; } diff --git a/src/cli/ArgumentsReader.h b/src/cli/ArgumentsReader.h index db4cbe3..282372a 100644 --- a/src/cli/ArgumentsReader.h +++ b/src/cli/ArgumentsReader.h @@ -9,20 +9,22 @@ #include "compilation/FrontendConfiguration.h" namespace Cli { -// import name / filepath -using SourcesList = std::unordered_map; class ArgumentsReader { - constexpr static auto kDefaultOutputName = "out"; constexpr static auto kSourceNamePathDelimiter = ":"; - static SourcesList parse_source_paths(std::vector sources); + static std::string get_default_output_name(Front::EmitType type); - static std::filesystem::path parse_output(const std::string& output); + static void parse_source_paths(const std::vector& sources, + Front::TeaFrontendConfiguration& config); + + static std::filesystem::path parse_output(std::string output, + Front::EmitType emit_type); static Front::EmitType get_emit_type(std::string_view name); public: static Front::TeaFrontendConfiguration read(int argc, char* argv[]); }; + } // namespace Cli diff --git a/src/cli/main.cpp b/src/cli/main.cpp index 21771d9..c3d8a58 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -1,13 +1,13 @@ #include #include #include -#include #include #include #include +#include +#include #include -#include #include "ArgumentsReader.h" #include "Constants.h" @@ -15,6 +15,7 @@ #include "errors/ExceptionsHandler.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/Support/FileSystem.h" +#include "utils/FileDescriptor.h" const bool Constants::is_installed_build = BUILD_FOR_INSTALLATION; @@ -22,25 +23,76 @@ namespace Cli { namespace fs = std::filesystem; class Main { - static void emit_ir(std::unique_ptr module, - const Front::TeaFrontendConfiguration& config) { - std::ofstream ofs; + static FileDescriptor get_output_fd(const std::filesystem::path& output_path, + Front::EmitType emit_type) { + const bool prohibit_stdout = + emit_type != Front::EmitType::AST && emit_type != Front::EmitType::IR; + const bool is_executable = emit_type == Front::EmitType::EXECUTABLE; + + if (output_path.empty()) { + if (prohibit_stdout) { + throw std::runtime_error( + fmt::format("Can't emit {} to stdout. Specify output file path.", + to_string(emit_type))); + } - std::ostream& out = [&]() -> std::ostream& { - if (config.output_file.empty()) { - return std::cout; + int stdout_fd = fileno(stdout); + if (stdout_fd == -1) { + throw std::runtime_error("Failed to open stdout."); } - ofs.open(config.output_file); - return ofs; - }(); + int fd = dup(stdout_fd); + if (fd == -1) { + throw std::runtime_error("Failed to duplicate stdout fd."); + } + + return FileDescriptor(fd); + } + + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; + if (is_executable) { + mode |= S_IXGRP | S_IXUSR; + } + + unlink(output_path.c_str()); + + int fd = open(output_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode); + + if (fd == -1) { + throw std::runtime_error( + fmt::format("Failed to open output file: {}.", strerror(errno))); + } + + return FileDescriptor(fd); + } + + static void add_std_includes(Front::TeaFrontendConfiguration& config) { + const char* std_filenames[] = {"io"}; + + for (const char* name : std_filenames) { + auto path = Constants::GetRuntimeFilePath( + fs::path(Constants::std_include_relative_path) / name); + path.replace_extension(".tea"); + + auto [_, inserted] = config.sources.emplace(name, path); - llvm::raw_os_ostream llvm_out(out); + if (!inserted) { + throw std::runtime_error( + fmt::format("Your module {:?} can't use the same name as a " + "standard library module.", + name)); + } + } + } + + static void emit_ir(std::unique_ptr module, + const FileDescriptor& fd) { + llvm::raw_fd_ostream llvm_out(fd.get(), false); module->print(llvm_out, nullptr); } - static void emit_binary(std::unique_ptr module, - const Front::TeaFrontendConfiguration& config) { + static void emit_object(std::unique_ptr module, + const FileDescriptor& fd) { llvm::InitializeNativeTarget(); llvm::InitializeNativeTargetAsmParser(); llvm::InitializeNativeTargetAsmPrinter(); @@ -70,34 +122,48 @@ class Main { module->setDataLayout(target_machine->createDataLayout()); - std::error_code error_code; - llvm::raw_fd_ostream dest( - config.output_file.empty() ? "-" : config.output_file.c_str(), - error_code, llvm::sys::fs::OF_None); - - if (error_code) { - throw std::runtime_error( - fmt::format("Could not open file: {}.", error_code.message())); - } - + llvm::raw_fd_ostream dest(fd.get(), false); llvm::legacy::PassManager pass; - auto file_type = llvm::CodeGenFileType::ObjectFile; - if (target_machine->addPassesToEmitFile(pass, dest, nullptr, file_type)) { + if (target_machine->addPassesToEmitFile( + pass, dest, nullptr, llvm::CodeGenFileType::ObjectFile)) { throw std::runtime_error( - "Target machine doesn't support binary output format."); + "Target machine doesn't support object output format."); } pass.run(*module); dest.flush(); } + static void emit_executable(std::unique_ptr module, + const FileDescriptor& fd) { + const auto tmp_fd = FileDescriptor::make_temp(); + + // write object file + emit_object(std::move(module), tmp_fd); + + // link with std + const auto std_path = + Constants::GetRuntimeFilePath(Constants::std_library_relative_filepath); + const auto link_command = + fmt::format(R"(clang++ "/dev/fd/{}" "{}" -o "/dev/fd/{}")", tmp_fd.get(), + std_path.string(), fd.get()); + + int link_status = system(link_command.c_str()); + if (link_status != 0) { + throw std::runtime_error("Failed to link with clang++."); + } + } + public: static int main(int argc, char* argv[]) { return ExceptionsHandler::execute([argc, argv] { auto config = ArgumentsReader::read(argc, argv); + add_std_includes(config); + auto front = Front::TeaFrontend(config); + auto llvm_module = front.compile(); if (config.emit_type == Front::EmitType::AST) { @@ -106,10 +172,21 @@ class Main { assert(llvm_module != nullptr); - if (config.emit_type == Front::EmitType::IR) { - emit_ir(std::move(llvm_module), config); - } else if (config.emit_type == Front::EmitType::BINARY) { - emit_binary(std::move(llvm_module), config); + const auto fd = get_output_fd(config.output_file, config.emit_type); + + switch (config.emit_type) { + case Front::EmitType::AST: + assert(false && "Should've been handled above"); + break; + case Front::EmitType::IR: + emit_ir(std::move(llvm_module), fd); + break; + case Front::EmitType::OBJECT: + emit_object(std::move(llvm_module), fd); + break; + case Front::EmitType::EXECUTABLE: + emit_executable(std::move(llvm_module), fd); + break; } }); } diff --git a/src/compilation/FrontendConfiguration.cpp b/src/compilation/FrontendConfiguration.cpp new file mode 100644 index 0000000..b3262aa --- /dev/null +++ b/src/compilation/FrontendConfiguration.cpp @@ -0,0 +1,17 @@ +#include "FrontendConfiguration.h" + +#include + +namespace Front { + +void TeaFrontendConfiguration::add_source(std::string name, + std::filesystem::path path) { + auto [_, inserted] = sources.emplace(name, path); + + if (!inserted) { + throw std::runtime_error(fmt::format( + "File {} was already included with different name.", path.c_str())); + } +} + +} // namespace Front diff --git a/src/compilation/FrontendConfiguration.h b/src/compilation/FrontendConfiguration.h index f4e083d..9263a09 100644 --- a/src/compilation/FrontendConfiguration.h +++ b/src/compilation/FrontendConfiguration.h @@ -4,14 +4,33 @@ #include #include +#include "errors/Helpers.h" + namespace Front { -enum class EmitType { AST, IR, BINARY }; +enum class EmitType { AST, IR, OBJECT, EXECUTABLE }; struct TeaFrontendConfiguration { std::unordered_map sources; std::filesystem::path output_file; EmitType emit_type; + + void add_source(std::string name, std::filesystem::path path); }; +inline std::string to_string(EmitType emit_type) { + switch (emit_type) { + case EmitType::AST: + return "AST"; + case EmitType::IR: + return "IR"; + case EmitType::OBJECT: + return "OBJ"; + case EmitType::EXECUTABLE: + return "EXE"; + } + + unreachable("All emit types should be enumerated above."); +} + } // namespace Front diff --git a/src/compilation/TeaFrontend.cpp b/src/compilation/TeaFrontend.cpp index 9e4edd9..f698472 100644 --- a/src/compilation/TeaFrontend.cpp +++ b/src/compilation/TeaFrontend.cpp @@ -246,7 +246,15 @@ std::unique_ptr TeaFrontend::compile() { llvm::Linker linker(*main_module); for (auto& module : llvm_modules_) { - linker.linkInModule(std::move(module)); + // remember module's name for error message + const std::string name = module->getName().str(); + + const bool has_error = linker.linkInModule(std::move(module)); + + if (has_error) { + throw std::runtime_error( + fmt::format("Error during linking of module {:?}.", name)); + } } llvm_modules_.clear(); diff --git a/src/sources/SourceManager.cpp b/src/sources/SourceManager.cpp index e27cdec..9593972 100644 --- a/src/sources/SourceManager.cpp +++ b/src/sources/SourceManager.cpp @@ -1,23 +1,30 @@ #include "SourceManager.h" #include +#include #include #include #include #include #include +#include #include #include #include +#include "utils/Defer.h" + SourceView SourceManager::load(const std::filesystem::path& path) { - int fd = open(path.c_str(), O_RDWR); + const int fd = open(path.c_str(), O_RDONLY); if (fd == -1) { - throw std::runtime_error("Failed to open source file."); + throw std::runtime_error( + fmt::format("Failed to open source file at {:?}.", path.string())); } + const auto defer = Defer([&fd] { close(fd); }); + struct stat statbuf; if (fstat(fd, &statbuf) == -1) { throw std::runtime_error("Failed to read file stat."); diff --git a/src/utils/Defer.h b/src/utils/Defer.h new file mode 100644 index 0000000..f7b56a0 --- /dev/null +++ b/src/utils/Defer.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +template +class Defer { + std::optional callback_; + + public: + explicit Defer(F callback) : callback_(std::move(callback)) {} + + Defer(const Defer&) = delete; + Defer& operator=(const Defer&) = delete; + + Defer(Defer&& other) noexcept + : callback_(std::exchange(other.callback_, std::nullopt)) {} + + Defer& operator=(Defer&&) = delete; + + ~Defer() { + if (callback_) { + std::invoke(*callback_); + } + } +}; diff --git a/src/utils/FileDescriptor.h b/src/utils/FileDescriptor.h new file mode 100644 index 0000000..3d90252 --- /dev/null +++ b/src/utils/FileDescriptor.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +class FileDescriptor { + int fd_; + + public: + explicit FileDescriptor(int fd) : fd_(fd) {} + + FileDescriptor(const FileDescriptor&) = delete; + FileDescriptor& operator=(const FileDescriptor&) = delete; + + FileDescriptor(FileDescriptor&& other) noexcept : fd_(other.fd_) { + other.fd_ = -1; + } + + static FileDescriptor make_temp() { + char filename[] = "/tmp/tlang.XXXXXX"; + int tmp_fd = mkstemp(filename); + + if (tmp_fd == -1) { + throw std::runtime_error( + fmt::format("Could not open file: {}.", strerror(errno))); + } + + unlink(filename); + + return FileDescriptor(tmp_fd); + } + + FileDescriptor& operator=(FileDescriptor&& other) noexcept { + fd_ = other.fd_; + other.fd_ = -1; + + return *this; + } + + int get() const { return fd_; } + + ~FileDescriptor() { + if (fd_ != -1) { + close(fd_); + } + } +}; diff --git a/std/CMakeLists.txt b/std/CMakeLists.txt new file mode 100644 index 0000000..3f6452e --- /dev/null +++ b/std/CMakeLists.txt @@ -0,0 +1,32 @@ +# Targets for standard library building and deployment + +file(GLOB_RECURSE STD_SOURCES CONFIGURE_DEPENDS "src/*.cpp" "src/*.h") + +add_library(std STATIC ${STD_SOURCES}) +set_target_properties(std PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY ${TEALANG_FILES_DIRECTORY}/std/lib +) + +file(GLOB_RECURSE STD_HEADERS CONFIGURE_DEPENDS + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/include + "include/*.tea" "include/*.team" +) + +set(STD_COPIED_HEADERS "") +foreach (header ${STD_HEADERS}) + set(copied_header ${TEALANG_FILES_DIRECTORY}/std/include/${header}) + + add_custom_command( + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/include/${header} ${copied_header} + + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/${header} + + OUTPUT ${copied_header} + COMMENT "Copying standard library header ${header}" + ) + + list(APPEND STD_COPIED_HEADERS ${copied_header}) +endforeach () + +add_custom_target(std_headers ALL DEPENDS ${STD_COPIED_HEADERS}) diff --git a/std/include/io.tea b/std/include/io.tea new file mode 100644 index 0000000..0e026c9 --- /dev/null +++ b/std/include/io.tea @@ -0,0 +1,3 @@ +export extern print: (x: i64) -> () + +export extern println: (x: i64) -> () \ No newline at end of file diff --git a/std/src/io.cpp b/std/src/io.cpp new file mode 100644 index 0000000..5b463d2 --- /dev/null +++ b/std/src/io.cpp @@ -0,0 +1,5 @@ +#include + +void print(long long value) { std::cout << value; } + +void println(long long value) { std::cout << value << std::endl; } diff --git a/testing.sh b/testing.sh index 4ed565c..28c50c5 100755 --- a/testing.sh +++ b/testing.sh @@ -4,7 +4,7 @@ mkdir build cd build || exit 1 cmake -DCMAKE_BUILD_TYPE=Debug .. || exit 1 -cmake --build . -t tests.unit cli tests.lit.execution.library || exit 1 +cmake --build . -t tests.unit cli || exit 1 ./tests/unit/tests.unit --gtest_output="xml:unit-report.xml" unit_status=$? diff --git a/tests/lit/CMakeLists.txt b/tests/lit/CMakeLists.txt index cf49797..6025e78 100644 --- a/tests/lit/CMakeLists.txt +++ b/tests/lit/CMakeLists.txt @@ -4,8 +4,6 @@ find_program(LLVM_FILE_CHECK NAMES "FileCheck" HINTS "${LLVM_TOOLS_BINARY_DIR}" find_program(LLVM_LLC NAMES "llc" HINTS "${LLVM_TOOLS_BINARY_DIR}" REQUIRED) find_program(LLVM_CLANG NAMES "clang++" REQUIRED) -add_subdirectory(execution) - # this handles variables substitution # like `@CMAKE_SOURCE_DIR@` configure_file( @@ -25,5 +23,5 @@ file(GENERATE add_custom_target( tests.lit COMMAND ${LLVM_LIT} "${CMAKE_CURRENT_BINARY_DIR}" -v --timeout=10 - DEPENDS cli tests.lit.execution.library + DEPENDS cli ) \ No newline at end of file diff --git a/tests/lit/errors/duplicate_main/main.tea b/tests/lit/errors/duplicate_main/main.tea new file mode 100644 index 0000000..66f7e18 --- /dev/null +++ b/tests/lit/errors/duplicate_main/main.tea @@ -0,0 +1,7 @@ +// RUN: not %tlang main:%S/main.tea second:%S/second.team --emit ir 2>&1 | %FileCheck %s + +// CHECK-NOT: ModuleID + +main: () -> i64 = { + return 0; +} diff --git a/tests/lit/errors/duplicate_main/second.team b/tests/lit/errors/duplicate_main/second.team new file mode 100644 index 0000000..9c0e27b --- /dev/null +++ b/tests/lit/errors/duplicate_main/second.team @@ -0,0 +1,3 @@ +main: () -> i64 = { + return 1; +} diff --git a/tests/lit/errors/module_conflict_with_std/culprit.team b/tests/lit/errors/module_conflict_with_std/culprit.team new file mode 100644 index 0000000..e69de29 diff --git a/tests/lit/errors/module_conflict_with_std/main.tea b/tests/lit/errors/module_conflict_with_std/main.tea new file mode 100644 index 0000000..57e6501 --- /dev/null +++ b/tests/lit/errors/module_conflict_with_std/main.tea @@ -0,0 +1,9 @@ +// RUN: not %tlang main:%S/main.tea io:%S/culprit.team 2>&1 | %FileCheck %s + +// CHECK: standard library + +import "io" + +main: () -> i64 = { + return 0; +} diff --git a/tests/lit/execution/CMakeLists.txt b/tests/lit/execution/CMakeLists.txt deleted file mode 100644 index b0f6579..0000000 --- a/tests/lit/execution/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -add_library(tests.lit.execution.library STATIC library.cpp) -set_target_properties( - tests.lit.execution.library PROPERTIES - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - LIBRARY_OUTPUT_NAME "library" -) \ No newline at end of file diff --git a/tests/lit/execution/executor.py b/tests/lit/execution/executor.py index 028e383..e59813f 100755 --- a/tests/lit/execution/executor.py +++ b/tests/lit/execution/executor.py @@ -7,23 +7,15 @@ def main(): - [_, tea_compiler, library, llc, clang, program] = sys.argv + [_, tea_compiler, _, _, program] = sys.argv with tempfile.TemporaryDirectory() as tempdir: - # compile tea file - ir = os.path.join(tempdir, "out.ll") - res = subprocess.run([tea_compiler, program, "--emit", "ir", "-o", ir]) - assert res.returncode == 0 - - # compile llvm ir into assembly - asm = os.path.join(tempdir, "out.s") - subprocess.run([llc, ir, "-o", asm]) - - # link our program and library + # compile exe = os.path.join(tempdir, "exe") - subprocess.run([clang, asm, library, "-o", exe]) + res = subprocess.run([tea_compiler, program, "--emit", "exe", "-o", exe]) + assert res.returncode == 0 - # run resulting program + # run subprocess.run([exe]) diff --git a/tests/lit/execution/library.cpp b/tests/lit/execution/library.cpp deleted file mode 100644 index 0be4a51..0000000 --- a/tests/lit/execution/library.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include - -void print(long long value) { - std::cout << value; -} - -void println(long long value) { - std::cout << value << "\n"; -} \ No newline at end of file diff --git a/tests/lit/lit.cfg.py b/tests/lit/lit.cfg.py index dc792b3..09a89b5 100644 --- a/tests/lit/lit.cfg.py +++ b/tests/lit/lit.cfg.py @@ -17,7 +17,7 @@ config.substitutions.append(("%clang", config.llvm_clang)) executor = os.path.join(config.src_root, 'tests/lit/execution/executor.py') -execute_order = f"{executor} {config.tea_path} {config.library} {config.llvm_llc} {config.llvm_clang}" +execute_order = f"{executor} {config.tea_path} {config.llvm_llc} {config.llvm_clang}" config.substitutions.append(("%execute", execute_order)) config.test_format = lit.formats.ShTest() diff --git a/tests/lit/lit.site.cfg.py.in b/tests/lit/lit.site.cfg.py.in index b0994e8..a761076 100644 --- a/tests/lit/lit.site.cfg.py.in +++ b/tests/lit/lit.site.cfg.py.in @@ -6,9 +6,6 @@ config.obj_root = r'@CMAKE_BINARY_DIR@' # tea compiler config.tea_path = r'$' -# library for execution tests -config.library = r'$' - # llvm tools config.llvm_filecheck = r'@LLVM_FILE_CHECK@' config.llvm_llc = r'@LLVM_LLC@' diff --git a/tests/unit/sources/SourceManagerTestCase.h b/tests/unit/sources/SourceManagerTestCase.h new file mode 100644 index 0000000..d751e35 --- /dev/null +++ b/tests/unit/sources/SourceManagerTestCase.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "sources/SourceManager.h" + +class SourceManagerTestCase : public ::testing::Test { + private: + std::vector created_; + + protected: + std::filesystem::path create_source(std::string_view content, + std::filesystem::perms permissions) { + auto name = ::testing::UnitTest::GetInstance()->current_test_info()->name(); + std::filesystem::path path = + std::filesystem::temp_directory_path() / + fmt::format("tlang_{}_{}.tea", name, created_.size()); + + std::ofstream(path) << content; + std::filesystem::permissions(path, permissions); + created_.push_back(path); + + return path; + } + + void TearDown() override { + for (const std::filesystem::path& path : created_) { + std::filesystem::remove(path); + } + } +}; diff --git a/tests/unit/sources/SourceManagerTests.cpp b/tests/unit/sources/SourceManagerTests.cpp new file mode 100644 index 0000000..fece05d --- /dev/null +++ b/tests/unit/sources/SourceManagerTests.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include "SourceManagerTestCase.h" + +namespace { + +constexpr std::string_view kProgram = "main: () -> i64 = { return 0; }"; + +// open returns the lowest unused descriptor, so this number grows whenever a +// descriptor is leaked. +int lowest_unused_descriptor() { + int fd = open("/dev/null", O_RDONLY); + close(fd); + return fd; +} + +} // namespace + +TEST_F(SourceManagerTestCase, test_it_loads_read_only_file) { + if (geteuid() == 0) { + GTEST_SKIP() << "root opens files regardless of their permissions"; + } + + auto path = create_source(kProgram, std::filesystem::perms::owner_read); + + SourceManager source_manager; + SourceView view = source_manager.load(path); + + ASSERT_EQ(view.string_view(), kProgram); +} + +TEST_F(SourceManagerTestCase, test_it_closes_descriptor_after_load) { + auto path = create_source(kProgram, std::filesystem::perms::owner_all); + + SourceManager source_manager; + + int before = lowest_unused_descriptor(); + source_manager.load(path); + int after = lowest_unused_descriptor(); + + ASSERT_EQ(before, after); +} diff --git a/tests/unit/utils/DeferTests.cpp b/tests/unit/utils/DeferTests.cpp new file mode 100644 index 0000000..c8bbee5 --- /dev/null +++ b/tests/unit/utils/DeferTests.cpp @@ -0,0 +1,15 @@ +#include + +#include "utils/Defer.h" + +TEST(DeferTests, move_construct) { + int counter = 0; + + { + auto a = Defer([&counter] { ++counter; }); + + auto b = std::move(a); + } + + ASSERT_EQ(counter, 1); +}