diff --git a/CMakeLists.txt b/CMakeLists.txt index 39aa735..5540b7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.31.2) +include(FetchContent) + set(BUILD_SHARED_LIBS OFF CACHE BOOL "Link libraries statically") if(CMAKE_CXX_COMPILER_ID MATCHES "CNU|Clang") @@ -12,8 +14,17 @@ if(MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endif() + #add_subdirectory("thirdparty/lib") +FetchContent_Declare( + cxxopts + GIT_REPOSITORY https://github.com/jarro2783/cxxopts.git + GIT_TAG "v3.3.1" +) + +FetchContent_MakeAvailable(cxxopts) + project(oxygen LANGUAGES CXX) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -27,9 +38,16 @@ add_executable(oxygen "${MY_SOURCES}") target_compile_definitions("${CMAKE_PROJECT_NAME}" PUBLIC RESOURCE_PATH="${CMAKE_CURRENT_SOURCE_DIR}/resources/") #target_compile_definitions("${CMAKE_PROJECT_NAME}" PUBLIC RESOURCE_PATH="./resources/") +# ----- Preprocessor ----- +if(include_dump_args_flag) + target_compile_definitions("${CMAKE_PROJECT_NAME}" PRIVATE DBG_OXY_DUMP_ARGS=1) +endif() + target_include_directories(oxygen PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/") #target_link_libraries(oxygen PRIVATE lib) +target_link_libraries(oxygen PRIVATE cxxopts::cxxopts) + # ----- LLVM ------ find_package(LLVM REQUIRED CONFIG) diff --git a/README.md b/README.md index b4c9636..8c45857 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # Oxygen A frankenstein compiler of C-like languages (C/C++) and Rust with its borrow system, using LLVM as backend. @@ -11,21 +10,27 @@ A frankenstein compiler of C-like languages (C/C++) and Rust with its borrow sys The project rely on CMake and LLVM, specifically, CMake 3.31.2, LLVM 19 respectively. It also uses C++ 23 standartd. -## Installation +Other dependencies are downloaded using CMake. + +## Building First, ensure you have CMake, LLVM and any sutable C++ compiler installed. Then clone repo and run CMake ```bash - git clone ... - cd oxygen mkdir build/ cd build cmake .. make ``` - + ## Usage/Examples -Well, as the compiler in earliest stage of development, there aren't much of an example to give, +The compiler accepts input file, as well as output file with `-o` flag. +Warnings can be ignored using `--Wignore` or produce errors instead, with `--Werror` flags. + +If defined `DBG_OXY_DUMP_ARGS` in source, or `-Dinclude_dump_args_flag=1` in CMake via shell, +the `-dArgs` flag will be availabe, to dump options structure. + +The compiler in earliest stage of development, so there aren't much of an example to give, nor what syntax or features it implies diff --git a/include/Args/CompilerOptions.hpp b/include/Args/CompilerOptions.hpp new file mode 100644 index 0000000..5b8a82f --- /dev/null +++ b/include/Args/CompilerOptions.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "Diagnostic/DiagnosticLevel.hpp" +#include "Diagnostic/DiagnosticsEngine.hpp" +#include +#include +#include + +struct CompilerOptions { + bool werror = false; // Produce errors instead of warnings + bool wignore = false; // Disable all warnings + + std::string inputFile; + std::string outputFile = "a.out"; + + int context = 1; + + /* -=-=-=- DEBUG -=-=-=- */ + bool dumpArgs = false; +}; + +CompilerOptions parseArguments(int argc, char** argv) { + CompilerOptions opts; + + cxxopts::Options options(argv[0], "Oxygen compiler"); + options.positional_help("input file").show_positional_help(); + + options.add_options() + ("h,help", "Print help") + + ("Wignore", "Disable all warnings", cxxopts::value(opts.wignore)) + ("Werror", "Produce errors instead of warnings", cxxopts::value(opts.werror)) + + ("o", "Output file", cxxopts::value(opts.outputFile)) + ("input", "Input file", cxxopts::value(opts.inputFile)) + + ("context", "Lines of context around diagnostic", cxxopts::value(opts.context)->default_value("1")) + +#ifdef DBG_OXY_DUMP_ARGS + ("dArgs", "Dumps parsed arguments to standard output", cxxopts::value(opts.dumpArgs)) +#endif +; // options.add_options() + + + options.parse_positional({"input"}); + + auto result = options.parse(argc, argv); + + if (result.count("help")) { + std::cout << options.help() << "\n"; + exit(0); + } + + if (!result.count("input")) { + std::cerr << "No input file provded. Use --help\n"; + exit(1); + } + + if(opts.dumpArgs) { + std::cout << "PARSED ARGS:\n"; + std::cout << " Wignore -> " << opts.wignore << "\n"; + std::cout << " Werror -> " << opts.werror << "\n"; + std::cout << " output -> " << opts.outputFile << "\n"; + std::cout << " input -> " << opts.inputFile << "\n"; + std::cout << " context -> " << opts.context << "\n"; + } + + return opts; +} diff --git a/include/Diagnostic/Colors.hpp b/include/Diagnostic/Colors.hpp new file mode 100644 index 0000000..d7defdf --- /dev/null +++ b/include/Diagnostic/Colors.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace Colors { + constexpr const char* Reset = "\033[0m"; + constexpr const char* Red = "\033[31m"; + constexpr const char* Yellow = "\033[33m"; + constexpr const char* Blue = "\033[34m"; + constexpr const char* Cyan = "\033[36m"; + constexpr const char* Gray = "\033[90m"; + + constexpr const char* Bold = "\033[1m"; + constexpr const char* Underline = "\033[4m"; +} diff --git a/include/Diagnostic/Diagnostic.hpp b/include/Diagnostic/Diagnostic.hpp new file mode 100644 index 0000000..0b6979d --- /dev/null +++ b/include/Diagnostic/Diagnostic.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Diagnostic/DiagnosticLevel.hpp" +#include "Diagnostic/SourceLocation.hpp" +#include "Diagnostic/DiagnosticNote.hpp" +#include +#include + +struct Diagnostic { + DiagnosticLevel level; + SourceLocation loc; + std::string message; + std::string suggestion; + int length = 1; + std::vector notes; +}; diff --git a/include/Diagnostic/DiagnosticLevel.hpp b/include/Diagnostic/DiagnosticLevel.hpp new file mode 100644 index 0000000..9a986e9 --- /dev/null +++ b/include/Diagnostic/DiagnosticLevel.hpp @@ -0,0 +1,7 @@ +#pragma once + +enum class DiagnosticLevel { + Info, + Warning, + Error +}; diff --git a/include/Diagnostic/DiagnosticNote.hpp b/include/Diagnostic/DiagnosticNote.hpp new file mode 100644 index 0000000..edb2da7 --- /dev/null +++ b/include/Diagnostic/DiagnosticNote.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "Diagnostic/SourceLocation.hpp" +#include + +struct DiagnosticNote { + SourceLocation loc; + int length = 1; + std::string message; +}; diff --git a/include/Diagnostic/DiagnosticsEngine.hpp b/include/Diagnostic/DiagnosticsEngine.hpp new file mode 100644 index 0000000..c968599 --- /dev/null +++ b/include/Diagnostic/DiagnosticsEngine.hpp @@ -0,0 +1,196 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "Diagnostic/Diagnostic.hpp" +#include "Diagnostic/SourceManager.hpp" +#include "Diagnostic/DiagnosticLevel.hpp" +#include "Diagnostic/SourceLocation.hpp" +#include "Diagnostic/Colors.hpp" + +class DiagnosticsEngine { +public: + DiagnosticsEngine(SourceManager& sm, int contextLines = 1, bool ignoreWarnings = false, bool warningsAreErrors = false) + : srcMgr(sm), context(contextLines), ignoreWarnings(ignoreWarnings), warningsAsErrors(warningsAreErrors) {} + + void report(const Diagnostic& d) { + if (d.level == DiagnosticLevel::Warning && ignoreWarnings) + return; + + Diagnostic newD; + if (d.level == DiagnosticLevel::Warning && warningsAsErrors) { + newD.loc = d.loc; + newD.level = DiagnosticLevel::Error; + newD.length = d.length; + newD.message = d.message; + newD.notes = d.notes; + newD.suggestion = d.suggestion; + } else { + newD = d; + } + + diags.push_back(newD); + printDiagnostic(newD); + } + + bool hasErrors() const { + for (auto &d : diags) { + if (d.level == DiagnosticLevel::Error) + return true; + } + return false; + } + + void setContext(int ctx) { context = ctx; } + +private: + SourceManager& srcMgr; + int context; + std::vector diags; + bool ignoreWarnings = false; + bool warningsAsErrors = false; + + static std::string expandTabs(const std::string& s, unsigned tabSize = 4) { + std::string out; + out.reserve(s.size()); + unsigned col = 0; + for (char ch : s) { + if (ch == '\t') { + unsigned spaces = tabSize - (col % tabSize); + out.append(spaces, ' '); + col += spaces; + } else { + out.push_back(ch); + ++col; + } + } + return out; + } + + static const char* levelToString(DiagnosticLevel level) { + switch (level) { + case DiagnosticLevel::Info: return "note"; + case DiagnosticLevel::Warning: return "warning"; + case DiagnosticLevel::Error: return "error"; + } + return ""; + } + + static const char* levelColor(DiagnosticLevel level) { + switch (level) { + case DiagnosticLevel::Info: return Colors::Blue; + case DiagnosticLevel::Warning: return Colors::Yellow; + case DiagnosticLevel::Error: return Colors::Red; + } + return ""; + } + + void printDiagnostic(const Diagnostic& d) { + // header -> file:line:column: : + std::ostringstream header; + header << d.loc.file << ":" << d.loc.line << ":" << d.loc.column << ": "; + + std::string lev = levelToString(d.level); + const char* color = levelColor(d.level); + + std::cerr << color << header.str() << lev << ": " << Colors::Bold << d.message << Colors::Reset << "\n"; + + if (!d.suggestion.empty()) { + std::cerr << Colors::Gray << " hint: " << Colors::Reset << d.suggestion << "\n"; + } + + printSourceSnippet(d.loc, d.length); + + for(const auto ¬e : d.notes) { + std::ostringstream noteHeader; + noteHeader << note.loc.file << ":" << note.loc.line << ":" << note.loc.column << ": "; + std::cerr << Colors::Cyan << noteHeader.str() << "note: " << Colors::Reset << note.message << "\n"; + printSourceSnippet(note.loc, note.length); + } + } + + void printSourceSnippet(const SourceLocation& loc, int length) { + const auto& lines = srcMgr.getLines(loc.fileId); + if(lines.empty()) { + std::cerr << Colors::Gray << " (source not available)\n" << Colors::Reset; + return; + } + + int totalLines = (int)lines.size(); + int lineIdx = std::max(1, loc.line); + if (lineIdx > totalLines) { + std::cerr << Colors::Gray << " (location outside file: line " << loc.line << " > " << totalLines << ")\n" << Colors::Reset; + return; + } + + int start = std::max(1, loc.line - context); + int end = std::min(totalLines, loc.line + context); + + int width = 1; + int maxLine = end; + while(maxLine >= 10) { maxLine /= 10; ++width; } + + for (int L = start; L <= end; ++L) { + std::string rawLine = lines[L - 1]; + std::string expanded = expandTabs(rawLine); + + std::ostringstream gutter; + if (L == loc.line) { + gutter << Colors::Red << ">" << Colors::Reset << " "; + } else { + gutter << " "; + } + + std::ostringstream ln; + ln << std::setw(width) << L; + + std::cerr << gutter.str() << Colors::Gray << ln.str() << " | " << Colors::Reset; + //std::cerr << expanded << "\n"; + + if (L == loc.line) { + int col = std::max(1, loc.column); + + int lineLen = (int)expanded.size(); + int startCol = std::min(col, lineLen + 1); + int maxAvail = std::max(0, lineLen - (startCol - 1)); + int caretCount = (length > 0) ? std::min(length, std::max(1, maxAvail)) : 1; + + int prefixLen = (startCol > 1) ? startCol - 1 : 0; + if (prefixLen > expanded.size()) prefixLen = expanded.size(); + + int errorLen = 0; + if (prefixLen < expanded.size()) { + errorLen = std::min((int)(expanded.size() - prefixLen), caretCount); + } + + if (errorLen > 0) { + std::cerr << expanded.substr(0, prefixLen) + << Colors::Bold << Colors::Red + << expanded.substr(prefixLen, errorLen) + << Colors::Reset + << expanded.substr(prefixLen + errorLen) << "\n"; + } else { + std::cerr << expanded << "\n"; + } + + std::ostringstream prefixSpaces; + prefixSpaces << " " << std::string(width, ' ') << " | "; + + std::cerr << prefixSpaces.str(); + + if (startCol > 1) + std::cerr << std::string(startCol - 1, ' '); + + std::cerr << Colors::Red << "^"; + for (int i = 1; i < caretCount; ++i) std::cerr << "~"; + std::cerr << Colors::Reset << "\n"; + } else { + std::cerr << expanded << "\n"; + } + } + } +}; diff --git a/include/Diagnostic/SourceLocation.hpp b/include/Diagnostic/SourceLocation.hpp new file mode 100644 index 0000000..d9cfa99 --- /dev/null +++ b/include/Diagnostic/SourceLocation.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct SourceLocation { + std::string file; + int fileId = -1; + int line; + int column; +}; diff --git a/include/Diagnostic/SourceManager.hpp b/include/Diagnostic/SourceManager.hpp new file mode 100644 index 0000000..31e53d5 --- /dev/null +++ b/include/Diagnostic/SourceManager.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class SourceManager { +public: + struct FileInfo { + std::string path; + std::vector lines; + std::string content; + }; + + int loadFile(const std::string& path) { + if (fileIDs.count(path)) { + return fileIDs[path]; + } + + std::ifstream ifs(path); + if (!ifs) return -1; + + FileInfo info; + info.path = path; + + std::ostringstream ss; + ss << ifs.rdbuf(); + info.content = ss.str(); + + std::string line; + std::istringstream ls(info.content); + while (std::getline(ls, line)) { + info.lines.push_back(line); + } + + int id = nextID++; + files[id] = std::move(info); + + return id; + } + + const FileInfo* getFile(int fileID) const { + auto it = files.find(fileID); + return (it == files.end() ? nullptr : &it->second); + } + + const std::vector& getLines(int fileID) const { + static const std::vector empty; + auto it = files.find(fileID); + return it != files.end() ? it->second.lines : empty; + } + + const std::string& getContent(int fileID) const { + static const std::string empty; + auto it = files.find(fileID); + return it != files.end() ? it->second.content : empty; + } + + bool exists(int fileID) const { + auto it = files.find(fileID); + return it != files.end(); + } + +private: + int nextID = 1; + std::map files; + std::map fileIDs; +}; diff --git a/src/main.cpp b/src/main.cpp index d1af452..fe611de 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,48 @@ -#include +#include "Args/CompilerOptions.hpp" +#include "Diagnostic/Diagnostic.hpp" +#include "Diagnostic/DiagnosticLevel.hpp" +#include "Diagnostic/DiagnosticsEngine.hpp" +#include "Diagnostic/SourceManager.hpp" -int main() -{ - std::println("hello, world!"); +int main(int argc, char** argv) { + CompilerOptions opts = parseArguments(argc, argv); + SourceManager sm; + DiagnosticsEngine diag(sm, opts.context, opts.wignore, opts.werror); + + auto mainId = sm.loadFile(opts.inputFile); + + if (!sm.exists(mainId)) { + Diagnostic d; + d.level = DiagnosticLevel::Error; + d.loc = { opts.inputFile, 1, 1 }; + d.message = "cannot open input file"; + d.length = 6; + d.suggestion = "make sure file exists and you have permissions to read it"; + diag.report(d); + return 1; + } + + { + Diagnostic w; + w.level = DiagnosticLevel::Warning; + w.loc = { opts.inputFile, mainId, 1, 8 }; + w.message = "unable to resolve 'std.io'"; + w.length = 6; + w.suggestion = "is it spelled corectly?"; + diag.report(w); + } + + { + Diagnostic d; + d.level = DiagnosticLevel::Error; + d.loc = { opts.inputFile, mainId, 4, 5 }; + d.message = "Unexpected identifier 'println'"; + d.length = 7; + d.suggestion = "check if it is defined, or module is imported"; + + d.notes.push_back({ { opts.inputFile, mainId, 1, 8 }, 6, "this unresovled import might contain identifier" }); + diag.report(d); + } + + return 0; } diff --git a/test.oxy b/test.oxy new file mode 100644 index 0000000..d4bd96b --- /dev/null +++ b/test.oxy @@ -0,0 +1,7 @@ +import std.io; + +func int main() { + println("hello, world"); + + return 0; +}