diff --git a/CMakeLists.txt b/CMakeLists.txt index 33875c9..9956a02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,9 +66,8 @@ target_include_directories(sapphirec PUBLIC src/external/tclap-1.2.5/include) target_include_directories(sapphirec PUBLIC src/external) target_precompile_headers(sapphirec PUBLIC src/sapphirepch.h) -llvm_map_components_to_libnames(llvm_libs support core irreader) +llvm_map_components_to_libnames(llvm_libs all core support irreader x86asmparser x86codegen x86desc x86disassembler x86info x86targetmca aarch64asmparser aarch64codegen aarch64desc aarch64disassembler aarch64info aarch64utils) -# Link against LLVM libraries target_link_libraries(sapphirec ${llvm_libs}) install(TARGETS sapphirec) diff --git a/src/Arguments.cpp b/src/Arguments.cpp index d49428f..5b3d34c 100644 --- a/src/Arguments.cpp +++ b/src/Arguments.cpp @@ -9,6 +9,7 @@ std::string Arguments::output_fname; bool Arguments::wimport; llvm::Triple Arguments::TargetTriple; std::string Arguments::cpu; +bool Arguments::emit_llvm; void Arguments::parse(int argc, char** argv) { @@ -17,20 +18,28 @@ void Arguments::parse(int argc, char** argv) { TCLAP::CmdLine command_line("The Sapphire compiler.", ' ', "0.1"); - TCLAP::UnlabeledValueArg input_fname_arg("file", "Input file.", true, "test.sp", "string"); + TCLAP::UnlabeledValueArg input_fname_arg("file", "Input file.", true, "program.sp", "string"); - TCLAP::ValueArg output_fname_arg("o", "output", "Output file.", false, "sp-output", "string"); + TCLAP::ValueArg output_fname_arg("o", "output", "Output file.", false, "output.o", "string"); TCLAP::ValueArg march_arg("", "march", "Architecture to compile for.", false, "native", "string"); TCLAP::ValueArg mcpu_arg("", "mcpu", "CPU to compile for.", false, "generic", "string"); TCLAP::ValueArg msystem_arg("", "msystem", "Operating System to compile for.", false, "native", "string"); + TCLAP::SwitchArg emit_llvm_arg("", "emit-llvm", "Emit LLVM IR instead of an object file."); + +#ifndef NO_BENCHMARKING TCLAP::SwitchArg mprofile_arg("", "mprofile", "Show execution times for functions."); +#endif TCLAP::SwitchArg wimport_arg("", "wimport", "Show a warning when trying to import an already imported file."); command_line.add(wimport_arg); + command_line.add(emit_llvm_arg); + +#ifndef NO_BENCHMARKING command_line.add(mprofile_arg); +#endif command_line.add(input_fname_arg); command_line.add(output_fname_arg); @@ -44,12 +53,15 @@ void Arguments::parse(int argc, char** argv) input_fname = input_fname_arg.getValue(); output_fname = output_fname_arg.getValue(); wimport = wimport_arg.getValue(); + emit_llvm = emit_llvm_arg.getValue(); cpu = mcpu_arg.getValue(); setTriple(march_arg.getValue(), msystem_arg.getValue()); +#ifndef NO_BENCHMARKING if (mprofile_arg.getValue()) __benchmark_impl::enable(); +#endif } catch (TCLAP::ArgException& e) { @@ -68,10 +80,11 @@ void Arguments::setTriple(const std::string& arch, const std::string& system) } if (system != "native") { - targetTriple.setOSAndEnvironmentName(system); + targetTriple.setOSName(system); } targetTriple.setVendor(llvm::Triple::VendorType::UnknownVendor); // let's leave it like that + targetTriple.setEnvironment(llvm::Triple::EnvironmentType::UnknownEnvironment); TargetTriple = targetTriple; } diff --git a/src/Arguments.h b/src/Arguments.h index 5b9af55..fd8d2cc 100644 --- a/src/Arguments.h +++ b/src/Arguments.h @@ -10,6 +10,7 @@ struct Arguments static std::string output_fname; static bool wimport; + static bool emit_llvm; static std::string cpu; diff --git a/src/Error.cpp b/src/Error.cpp index b65287e..0aa6a34 100644 --- a/src/Error.cpp +++ b/src/Error.cpp @@ -85,6 +85,18 @@ void Error::show_import_lines(const Location& loc, void (*import_line_printer)(c exit(1); } +void Error::throw_warning_without_location(const std::string& details) +{ + std::cerr << "\033[1;1m"; + + std::cerr << "\033[33;49m"; + std::cerr << "warning: "; + + std::cerr << "\033[0;0m"; + std::cerr << details; + std::cerr << std::endl; +} + void Error::throw_warning(const Location& loc, const std::string line_text, const std::string& details) { show_import_lines(loc, show_import_line, std::cout); diff --git a/src/Error.h b/src/Error.h index f6efd57..3cc3775 100644 --- a/src/Error.h +++ b/src/Error.h @@ -11,6 +11,8 @@ void show_import_line(const Location& loc, std::ostream& output_stream); void throw_warning(const Location& loc, const std::string line_text, const std::string& details); +void throw_warning_without_location(const std::string& details); + void show_import_lines(const Location& loc, void (*import_line_printer)(const Location&, std::ostream&), std::ostream& stream); } // namespace Error diff --git a/src/IRBuilder.cpp b/src/IRBuilder.cpp index cc585d5..ed9ebd2 100644 --- a/src/IRBuilder.cpp +++ b/src/IRBuilder.cpp @@ -1,8 +1,17 @@ #include "IRBuilder.h" #include "Arguments.h" +#include "Error.h" #include "llvm/IR/Function.h" +#include "llvm/IR/LegacyPassManager.h" #include "llvm/IR/Value.h" #include "llvm/IR/Verifier.h" +#include "llvm/MC/TargetRegistry.h" +#include "llvm/Support/CodeGen.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Host.h" +#include "llvm/Support/TargetSelect.h" +#include "llvm/Target/TargetMachine.h" +#include "llvm/Target/TargetOptions.h" IRBuilder::IRBuilder() { @@ -31,12 +40,76 @@ void IRBuilder::create_main_function(std::shared_ptr expression) llvm::verifyFunction(*main); } -std::string IRBuilder::getGeneratedIR() +void IRBuilder::resolveToLLVMIR(std::string path) { - std::string str; - llvm::raw_string_ostream oss(str); + std::error_code EC; + llvm::raw_fd_ostream dest(path, EC, llvm::sys::fs::OF_None); - module->print(oss, nullptr); + if (EC) + { + Error::throw_error_without_location("Could not open file " + Arguments::output_fname + " :" + EC.message()); + } - return oss.str(); -} \ No newline at end of file + module->print(dest, nullptr); + dest.flush(); +} + +void IRBuilder::resolveToObjectFile(std::string path) +{ + // edit this section when adding support for more architectures + LLVMInitializeX86TargetInfo(); + LLVMInitializeAArch64TargetInfo(); + LLVMInitializeX86Target(); + LLVMInitializeAArch64Target(); + LLVMInitializeX86TargetMC(); + LLVMInitializeAArch64TargetMC(); + LLVMInitializeX86AsmParser(); + LLVMInitializeAArch64AsmParser(); + LLVMInitializeX86AsmPrinter(); + LLVMInitializeAArch64AsmPrinter(); + + std::string TargetTriple = Arguments::TargetTriple.getTriple(); + + module->setTargetTriple(TargetTriple); + + std::string err; + auto Target = llvm::TargetRegistry::lookupTarget(TargetTriple, err); + if (!Target) + { + Error::throw_error_without_location(err); + } + std::string CPU; + if (Arguments::cpu == "native") + { + CPU = llvm::sys::getHostCPUName(); + Error::throw_warning_without_location("Using host CPU: " + CPU); + } + else + CPU = Arguments::cpu; + auto Features = ""; + + llvm::TargetOptions opt; + auto RM = llvm::Optional(); + auto TheTargetMachine = Target->createTargetMachine(TargetTriple, CPU, Features, opt, RM); + + module->setDataLayout(TheTargetMachine->createDataLayout()); + + std::error_code EC; + llvm::raw_fd_ostream dest(path, EC, llvm::sys::fs::OF_None); + + if (EC) + { + Error::throw_error_without_location("Could not open file " + Arguments::output_fname + " :" + EC.message()); + } + + llvm::legacy::PassManager pass; + auto FileType = llvm::CGFT_ObjectFile; + + if (TheTargetMachine->addPassesToEmitFile(pass, dest, nullptr, FileType)) + { + Error::throw_error_without_location("Cannot emit object file"); + } + + pass.run(*module); + dest.flush(); +} diff --git a/src/IRBuilder.h b/src/IRBuilder.h index 98e883b..f28baee 100644 --- a/src/IRBuilder.h +++ b/src/IRBuilder.h @@ -19,5 +19,7 @@ class IRBuilder llvm::IRBuilder<>* getBuilder(); - std::string getGeneratedIR(); + void resolveToLLVMIR(std::string path); + + void resolveToObjectFile(std::string path); }; \ No newline at end of file diff --git a/src/sapphire.cpp b/src/sapphire.cpp index b2c4cf3..732adc6 100644 --- a/src/sapphire.cpp +++ b/src/sapphire.cpp @@ -48,5 +48,7 @@ int main(int argc, char** argv) builder.create_main_function(ast); } - std::cout << builder.getGeneratedIR(); + if (Arguments::emit_llvm) builder.resolveToLLVMIR(Arguments::output_fname); + else + builder.resolveToObjectFile(Arguments::output_fname); } diff --git a/src/utils.cpp b/src/utils.cpp index 55ff7b0..c7f3731 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -23,6 +23,8 @@ std::string to_string(const float& value) return result.str(); } +#ifndef NO_BENCHMARKING + __benchmark_impl::__benchmark_impl(std::string_view __function_name) : m_FunctionName(__function_name) { m_InternalBenchmarkingEnabled = m_BenchmarkingEnabled; @@ -59,4 +61,6 @@ void __benchmark_impl::enable() m_BenchmarkingEnabled = true; } -bool __benchmark_impl::m_BenchmarkingEnabled; \ No newline at end of file +bool __benchmark_impl::m_BenchmarkingEnabled; + +#endif \ No newline at end of file diff --git a/src/utils.h b/src/utils.h index 050db1c..c64c267 100644 --- a/src/utils.h +++ b/src/utils.h @@ -25,6 +25,7 @@ std::string to_string(const int& value); std::string to_string(const float& value); /* Benchmarking utilities. */ +#ifndef NO_BENCHMARKING class __benchmark_impl { @@ -44,4 +45,7 @@ class __benchmark_impl bool m_InternalBenchmarkingEnabled; }; -#define benchmark(message) __benchmark_impl __benchmark_impl_timer(message) \ No newline at end of file +#define benchmark(message) __benchmark_impl __benchmark_impl_timer(message) +#else +#define benchmark(message) +#endif \ No newline at end of file diff --git a/test.py b/test.py index 57b523a..016e904 100755 --- a/test.py +++ b/test.py @@ -2,8 +2,8 @@ import subprocess import json import os -import argparse import sys +import shutil def load_test_case(filename: str) -> dict: @@ -23,9 +23,9 @@ def test_test_case(test_case: dict) -> bool: compiler_stdout = compiler["stdout"] compiler_stderr = compiler["stderr"] command = " ".join(["build/sapphirec", filepath, "-o", - ".tests-bin/exe"] + extra_flags) + ".tests-bin/output.o"] + extra_flags) print(f"-> Running command: {command}") - compile_task = subprocess.Popen(["build/sapphirec", filepath, "-o", ".tests-bin/exe"] + extra_flags, + compile_task = subprocess.Popen(["build/sapphirec", filepath, "-o", ".tests-bin/output.o"] + extra_flags, stdout=subprocess.PIPE, stderr=subprocess.PIPE) retcode = compile_task.wait() retstdout = compile_task.stdout.read().decode('utf-8') @@ -42,6 +42,33 @@ def test_test_case(test_case: dict) -> bool: print("-> Test failed: compiler stderr does not match test case") print(retstderr) return False + runtime = test_case.get("run", False) + if runtime is not False: + print("-> Running command: gcc .tests-bin/output.o -o .tests-bin/output") + link_task = subprocess.Popen(["gcc", ".tests-bin/output.o", "-o", ".tests-bin/output"] + extra_flags, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if link_task.wait() != 0: + print(f"-> Failed to link program") + return False + print(f"-> Running command: .tests-bin/output") + runtime_task = subprocess.Popen([".tests-bin/output"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + runtime_code = runtime_task.wait() + runtime_stdout = runtime_task.stdout.read().decode('utf-8') + runtime_stderr = runtime_task.stderr.read().decode('utf-8') + test_code = runtime["exit-code"] + if runtime_code != runtime["exit-code"]: + print( + f"-> Test failed: program exited with code {str(runtime_code)} (expected {str(test_code)})") + return False + if runtime_stdout != runtime["stdout"]: + print("-> Test failed: program stdout does not match test case") + print(retstdout) + return False + if runtime_stderr != runtime["stderr"]: + print("-> Test failed: program stderr does not match test case") + print(retstderr) + return False print("-> Test succeeded") return True @@ -50,7 +77,7 @@ test_cases = [] def run_tests(): - os.mkdir(".tests-bin") + os.makedirs(".tests-bin", exist_ok=True) for filename in os.listdir("tests"): if filename.endswith(".json"): test_cases.append(load_test_case(os.path.join("tests", filename))) @@ -60,21 +87,21 @@ def run_tests(): for test_case in test_cases: if not test_test_case(test_case): print("-> Test suite failed") - os.rmdir(".tests-bin") + shutil.rmtree(".tests-bin") sys.exit(1) else: print("-> Test suite succeeded") - os.rmdir(".tests-bin") + shutil.rmtree(".tests-bin") def create_test(filename: str, extra_flags: list): - os.mkdir(".tests-bin") + os.makedirs(".tests-bin", exist_ok=True) test_case = {} test_case["file"] = filename filepath = os.path.join("tests", filename) compiler = {} compiler["flags"] = extra_flags - compile_task = subprocess.Popen(["build/sapphirec", filepath, "-o", ".tests-bin/exe"] + extra_flags, + compile_task = subprocess.Popen(["build/sapphirec", filepath, "-o", ".tests-bin/output.o"] + extra_flags, stdout=subprocess.PIPE, stderr=subprocess.PIPE) retcode = compile_task.wait() retstdout = compile_task.stdout.read().decode('utf-8') @@ -83,11 +110,26 @@ def create_test(filename: str, extra_flags: list): compiler["stdout"] = retstdout compiler["stderr"] = retstderr test_case["compile"] = compiler + if retcode == 0: + link_task = subprocess.Popen(["gcc", ".tests-bin/output.o", "-o", ".tests-bin/output"] + extra_flags, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + link_status = link_task.wait() + if link_status == 0: + program = {} + runtime_task = subprocess.Popen([".tests-bin/output"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + runtime_code = runtime_task.wait() + runtime_stdout = runtime_task.stdout.read().decode('utf-8') + runtime_stderr = runtime_task.stderr.read().decode('utf-8') + program["exit-code"] = runtime_code + program["stdout"] = runtime_stdout + program["stderr"] = runtime_stderr + test_case["run"] = program ofilepath = ".".join(filepath.split(".")[:-1]) + ".json" ofile = open(ofilepath, "w+") json.dump(test_case, ofile) ofile.close() - os.rmdir(".tests-bin") + shutil.rmtree(".tests-bin") if len(sys.argv) < 2: diff --git a/tests/calc.json b/tests/calc.json index e8b462d..7473b2c 100644 --- a/tests/calc.json +++ b/tests/calc.json @@ -1 +1 @@ -{"file": "calc.sp", "compile": {"flags": [], "exit-code": 0, "stdout": "; ModuleID = 'tests/calc.sp'\nsource_filename = \"tests/calc.sp\"\n\ndefine i32 @main() {\nentry:\n ret i32 16\n}\n", "stderr": ""}} \ No newline at end of file +{"file": "calc.sp", "compile": {"flags": [], "exit-code": 0, "stdout": "", "stderr": ""}, "run": {"exit-code": 16, "stdout": "", "stderr": ""}} \ No newline at end of file diff --git a/tests/calc.sp b/tests/calc.sp index 17645a9..46ec7e5 100644 --- a/tests/calc.sp +++ b/tests/calc.sp @@ -1 +1 @@ -1 + 3 * 5 +1 + 3 * 5 \ No newline at end of file diff --git a/tests/wimport.json b/tests/wimport.json index 20b2156..b2f32a7 100644 --- a/tests/wimport.json +++ b/tests/wimport.json @@ -1 +1 @@ -{"file": "wimport.sp", "compile": {"flags": [], "exit-code": 1, "stdout": "", "stderr": "\u001b[1;1mtests/wimport.sp:1:22: \u001b[31;49merror: \u001b[0;0mexpected a number\n1 \n \u001b[31;49m^\u001b[0;0m\n"}} \ No newline at end of file +{"file": "wimport.sp", "compile": {"flags": ["--wimport"], "exit-code": 1, "stdout": "\u001b[1;1mtests/wimport.sp:1:8: \u001b[33;49mwarning: \u001b[0;0mfile already imported, skipping\n1 import tests/wimport;\n \u001b[33;49m^\u001b[0;0m\n", "stderr": "\u001b[1;1mtests/wimport.sp:1:22: \u001b[31;49merror: \u001b[0;0mexpected a number\n1 \n \u001b[31;49m^\u001b[0;0m\n"}} \ No newline at end of file