diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..d7f8eb9 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,92 @@ +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-exception-escape, + cert-*, + -cert-err58-cpp, + clang-analyzer-*, + concurrency-*, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-const-or-ref-data-members, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-avoid-do-while, + google-*, + -google-readability-todo, + -google-runtime-references, + misc-*, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, + modernize-*, + -modernize-use-trailing-return-type, + -modernize-avoid-c-arrays, + performance-*, + readability-*, + -readability-identifier-length, + -readability-magic-numbers, + -readability-function-cognitive-complexity, + -readability-uppercase-literal-suffix, + -readability-else-after-return, + -readability-braces-around-statements, + -google-readability-braces-around-statements, + -misc-include-cleaner + +WarningsAsErrors: '' + +CheckOptions: + # Naming conventions + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.VariableCase + value: camelBack + - key: readability-identifier-naming.ParameterCase + value: camelBack + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.MemberCase.Prefix + value: m_ + - key: readability-identifier-naming.ConstantMemberCase + value: camelBack + - key: readability-identifier-naming.ConstantMemberPrefix + value: m_ + - key: readability-identifier-naming.ConstantCase + value: camelBack + - key: readability-identifier-naming.EnumConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.GlobalConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + + # Line length + - key: readability-function-size.LineThreshold + value: 200 + - key: readability-function-size.StatementThreshold + value: 200 + + # Misc settings + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: performance-move-const-arg.CheckTriviallyCopyableMove + value: true + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: true + +# Exclude third-party code and build artifacts +HeaderFilterRegex: '^.*/(src|test)/.*\.(h|hpp)$' + +# Suppress warnings from system headers +SystemHeaders: false diff --git a/.github/scripts/check-format.sh b/.github/scripts/check-format.sh new file mode 100755 index 0000000..a736d3e --- /dev/null +++ b/.github/scripts/check-format.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +echo "Finding changed C++ files against base branch: main" +CHANGED_FILES=$(git diff --name-only --diff-filter=AM "origin/main...HEAD" | \ + grep -E '\.(cpp|hpp|h)$' || true) + +# Filter to only files that exist (not deleted) +EXISTING_FILES="" +for file in $CHANGED_FILES; do + if [ -f "$file" ]; then + EXISTING_FILES="$EXISTING_FILES $file" + fi +done + +if [ -z "$EXISTING_FILES" ]; then + echo "No C++ files changed, skipping format check" + exit 0 +fi + +echo "Checking formatting for: $EXISTING_FILES" + +# shellcheck disable=SC2086 +clang-format --dry-run --Werror $EXISTING_FILES + +echo "All files are properly formatted!" diff --git a/.github/scripts/generate-coverage.sh b/.github/scripts/generate-coverage.sh new file mode 100755 index 0000000..f249d3c --- /dev/null +++ b/.github/scripts/generate-coverage.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +cd build + +echo "Running tests with coverage instrumentation..." +export LLVM_PROFILE_FILE="caesar-%p.profraw" +./caesar_test + +echo "Checking for profraw files..." +if ! ls caesar-*.profraw 1> /dev/null 2>&1; then + echo "ERROR: No .profraw files found!" + exit 1 +fi + +echo "Merging profile data..." +/usr/bin/llvm-profdata-21 merge -sparse caesar-*.profraw -o caesar.profdata + +echo "Generating coverage report (text summary)..." +/usr/bin/llvm-cov-21 report ./caesar_test \ + -instr-profile=caesar.profdata \ + -ignore-filename-regex='test/.*' \ + -ignore-filename-regex='.*/Catch2/.*' + +echo "Generating detailed HTML report..." +/usr/bin/llvm-cov-21 show ./caesar_test \ + -instr-profile=caesar.profdata \ + -format=html \ + -output-dir=coverage-report \ + -ignore-filename-regex='test/.*' \ + -ignore-filename-regex='.*/Catch2/.*' \ + -show-line-counts-or-regions \ + -show-instantiations + +echo "Generating per-file coverage summary..." +echo "### Per-File Coverage Summary" > coverage-summary.txt +/usr/bin/llvm-cov-21 report ./caesar_test \ + -instr-profile=caesar.profdata \ + -ignore-filename-regex='test/.*' \ + -ignore-filename-regex='.*/Catch2/.*' \ + >> coverage-summary.txt + +echo "Coverage report generated successfully" +cat coverage-summary.txt diff --git a/.github/scripts/run-clang-tidy.sh b/.github/scripts/run-clang-tidy.sh new file mode 100755 index 0000000..5f1fa50 --- /dev/null +++ b/.github/scripts/run-clang-tidy.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +echo "Finding changed C++ files against base branch: main" +CHANGED_FILES=$(git diff --name-only --diff-filter=AM "origin/main...HEAD" | \ + grep -E '\.(cpp)$' | \ + grep -v '^test/' || true) + +# Filter to only files that exist (not deleted) +EXISTING_FILES="" +for file in $CHANGED_FILES; do + if [ -f "$file" ]; then + EXISTING_FILES="$EXISTING_FILES $file" + fi +done + +if [ -z "$EXISTING_FILES" ]; then + echo "No C++ files changed, skipping clang-tidy" + exit 0 +fi + +echo "Changed C++ files: $EXISTING_FILES" +echo "Running clang-tidy..." + +# Run clang-tidy on changed files +# shellcheck disable=SC2086 +clang-tidy -p build --warnings-as-errors='*' $EXISTING_FILES + +echo "clang-tidy completed successfully" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e0e9934 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,137 @@ +name: CI + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + pull-requests: write + +env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_COMMIT: 'b1e15efef6758eaa0beb0a8732cfa66f6a68a81d' + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build ccache + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 21 all + sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-21 100 + sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-21 100 + sudo update-alternatives --install /usr/bin/clang-scan-deps clang-scan-deps /usr/bin/clang-scan-deps-21 100 + sudo update-alternatives --install /usr/bin/llvm-profdata llvm-profdata /usr/bin/llvm-profdata-21 100 + sudo update-alternatives --install /usr/bin/llvm-cov llvm-cov /usr/bin/llvm-cov-21 100 + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ runner.os }}-ccache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-ccache- + + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: ${{ env.VCPKG_ROOT }} + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT }} + + - name: Configure CMake + run: cmake -B build --preset debug -DENABLE_COVERAGE=ON + env: + CC: /usr/bin/clang-21 + CXX: /usr/bin/clang++-21 + CMAKE_CXX_COMPILER_LAUNCHER: ccache + + - name: Build + run: cmake --build build + + - name: Run tests and generate coverage + run: .github/scripts/generate-coverage.sh + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: | + build/coverage-report/ + build/coverage-summary.txt + retention-days: 30 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + build/ + !build/vcpkg_installed/ + retention-days: 1 + + lint: + name: Lint + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 21 all + sudo update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-21 100 + sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-21 100 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: build + + - name: Check formatting on changed files + run: .github/scripts/check-format.sh + + - name: Run clang-tidy on changed files + run: .github/scripts/run-clang-tidy.sh + + coverage-comment: + name: Post Coverage Comment + runs-on: ubuntu-latest + needs: build + + steps: + - name: Download coverage report + uses: actions/download-artifact@v4 + with: + name: coverage-report + + - name: Comment coverage summary on PR + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const summary = fs.readFileSync('coverage-summary.txt', 'utf8'); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '## Code Coverage Report\n\n```\n' + summary + '\n```\n\nDetailed HTML report available in artifacts.' + }); diff --git a/.gitignore b/.gitignore index 969e7ee..ad06f44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,13 @@ .cache build/ +vcpkg-manifest-install.log +vcpkg_installed + +# Coverage +coverage/ +*.profraw +*.profdata +*.gcda +*.gcno +*.gcov +*.info diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f1cff4..e37928e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.31.0) +cmake_minimum_required(VERSION 3.28.0) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -6,30 +6,64 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project(caesar VERSION 0.1.0) -set(CMD_SOURCES - src/cmd/error.cpp - src/cmd/scanner.cpp - src/cmd/error.hpp - src/cmd/scanner.hpp - src/cmd/token.hpp - src/cmd/util.cpp - src/cmd/util.hpp - src/cmd/expr.hpp - src/cmd/astprint.cpp - src/cmd/astprint.hpp - src/cmd/parser.hpp - src/cmd/parser.cpp - src/cmd/parse_error.hpp - src/cmd/interpreter.cpp - src/cmd/interpreter.hpp - src/cmd/stmnt.hpp - src/cmd/environment.hpp - src/cmd/environment.cpp) - -set(CORE_SOURCES src/core/macho.cpp src/core/macho.hpp src/core/util.hpp) -set(DWARF_SOURCES src/core/dwarf/context.cpp src/core/dwarf/context.hpp src/core/dwarf/alloc.hpp) -add_executable(caesar src/main.cpp ${CMD_SOURCES} ${CORE_SOURCES} - ${DWARF_SOURCES}) - -find_package(libdwarf CONFIG REQUIRED) -target_link_libraries(caesar PUBLIC libdwarf::dwarf) +# Optional: Enable code coverage +option(ENABLE_COVERAGE "Enable code coverage" OFF) + +add_subdirectory(src/cmd) +add_subdirectory(src/core) + +# Add coverage flags if enabled +if(ENABLE_COVERAGE) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # Clang + set(COVERAGE_COMPILE_FLAGS -fprofile-instr-generate -fcoverage-mapping) + set(COVERAGE_LINK_FLAGS -fprofile-instr-generate) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + # GCC + set(COVERAGE_COMPILE_FLAGS --coverage) + set(COVERAGE_LINK_FLAGS --coverage) + endif() + + # Apply coverage flags to libraries + target_compile_options(caesar_cmd PUBLIC ${COVERAGE_COMPILE_FLAGS}) + target_link_options(caesar_cmd PUBLIC ${COVERAGE_LINK_FLAGS}) + target_compile_options(caesar_core PUBLIC ${COVERAGE_COMPILE_FLAGS}) + target_link_options(caesar_core PUBLIC ${COVERAGE_LINK_FLAGS}) + + message(STATUS "Code coverage enabled") +endif() + +add_executable(caesar src/main.cpp) +target_link_libraries(caesar PRIVATE caesar_cmd caesar_core) + +# Optional: Build tests +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR OR CAESAR_BUILD_TESTS) + option(CAESAR_BUILD_TESTS "Build tests" ON) +else() + option(CAESAR_BUILD_TESTS "Build tests" OFF) +endif() + +if(CAESAR_BUILD_TESTS) + find_package(Catch2 CONFIG REQUIRED) + add_executable( + caesar_test + test/cmd/test_environment.cpp + test/cmd/test_interpreter.cpp + test/cmd/test_object.cpp + test/cmd/test_parser.cpp + test/cmd/test_scanner.cpp + ) + + target_link_libraries(caesar_test PRIVATE caesar_cmd caesar_core Catch2::Catch2WithMain) + + # Apply coverage flags to test executable if coverage is enabled + if(ENABLE_COVERAGE) + target_compile_options(caesar_test PUBLIC ${COVERAGE_COMPILE_FLAGS}) + target_link_options(caesar_test PUBLIC ${COVERAGE_LINK_FLAGS}) + endif() + + include(CTest) + include(Catch) + catch_discover_tests(caesar_test) + enable_testing() +endif() diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 0000000..3486d6c --- /dev/null +++ b/coverage.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Script to generate code coverage reports + +set -e + +BUILD_DIR="build" +COVERAGE_DIR="coverage" + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}==> Building with coverage enabled...${NC}" +cmake -B ${BUILD_DIR} -DENABLE_COVERAGE=ON -DCAESAR_BUILD_TESTS=ON +cmake --build ${BUILD_DIR} + +echo -e "${BLUE}==> Running tests...${NC}" +cd ${BUILD_DIR} +./caesar_test + +# Detect compiler and generate appropriate coverage report +if [[ $(cmake --system-information | grep CMAKE_CXX_COMPILER_ID) =~ "Clang" ]] || command -v llvm-cov &> /dev/null; then + echo -e "${BLUE}==> Generating coverage report with llvm-cov...${NC}" + + # Merge profiling data + xcrun llvm-profdata merge -sparse default.profraw -o coverage.profdata + + # Generate HTML report + mkdir -p ../${COVERAGE_DIR} + xcrun llvm-cov show ./caesar_test \ + -instr-profile=coverage.profdata \ + -format=html \ + -output-dir=../${COVERAGE_DIR} \ + -ignore-filename-regex="(test/|vcpkg/|build/)" \ + -show-line-counts-or-regions + + # Generate summary + xcrun llvm-cov report ./caesar_test \ + -instr-profile=coverage.profdata \ + -ignore-filename-regex="(test/|vcpkg/|build/)" + + echo -e "${GREEN}==> Coverage report generated in ${COVERAGE_DIR}/index.html${NC}" + echo -e "${GREEN}==> Open with: open ${COVERAGE_DIR}/index.html${NC}" + +elif command -v gcov &> /dev/null && command -v lcov &> /dev/null; then + echo -e "${BLUE}==> Generating coverage report with lcov...${NC}" + + # Generate coverage data + lcov --capture --directory . --output-file coverage.info + + # Filter out system headers and test files + lcov --remove coverage.info '/usr/*' '*/test/*' '*/vcpkg/*' --output-file coverage_filtered.info + + # Generate HTML report + mkdir -p ../${COVERAGE_DIR} + genhtml coverage_filtered.info --output-directory ../${COVERAGE_DIR} + + echo -e "${GREEN}==> Coverage report generated in ${COVERAGE_DIR}/index.html${NC}" + echo -e "${GREEN}==> Open with: open ${COVERAGE_DIR}/index.html${NC}" +else + echo "Error: No coverage tool found. Install llvm-cov or lcov/gcov" + exit 1 +fi + +cd .. diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt new file mode 100644 index 0000000..6a9093e --- /dev/null +++ b/src/cmd/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(caesar_cmd STATIC + error.cpp + error.hpp + scanner.cpp + scanner.hpp + token.hpp + util.cpp + util.hpp + expr.hpp + parser.hpp + parser.cpp + interpreter.cpp + interpreter.hpp + stmnt.hpp + environment.hpp + environment.cpp + callable.hpp + stdlib.hpp + object.hpp + formatter.hpp +) + +target_include_directories(caesar_cmd PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/cmd/astprint.cpp b/src/cmd/astprint.cpp deleted file mode 100644 index a67d52a..0000000 --- a/src/cmd/astprint.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "astprint.hpp" - -Object AstPrinter::print(const std::unique_ptr& expr) { - return expr->accept(this); -} - -Object AstPrinter::visitBinaryExpr(const Binary& expr) { - return parenthesise(expr.m_op.m_lexeme, expr.m_left, expr.m_right); -} - -Object AstPrinter::visitUnaryExpr(const Unary& expr) { - return parenthesise(expr.m_op.m_lexeme, expr.m_right); -} - -Object AstPrinter::visitGroupingExpr(const Grouping& expr) { - return parenthesise("group", expr.m_expr); -} - -Object AstPrinter::visitLiteralExpr(const Literal& expr) { - return std::format("{}", expr); -} diff --git a/src/cmd/astprint.hpp b/src/cmd/astprint.hpp deleted file mode 100644 index 6cba9a9..0000000 --- a/src/cmd/astprint.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef ASTPRINT_H -#define ASTPRINT_H -#include "expr.hpp" - -class AstPrinter : public IExprVisitor { - private: - template - requires(std::same_as, std::unique_ptr> && ...) - std::string parenthesise(const std::string& name, Args&... expr) { - std::string result; - result.append("(").append(name); - (result.append(std::format(" {}", expr->accept(this))), ...); - result.append(")"); - return result; - } - - public: - Object print(const std::unique_ptr& expr); - Object visitBinaryExpr(const Binary& expr) override; - Object visitUnaryExpr(const Unary& expr) override; - Object visitGroupingExpr(const Grouping& expr) override; - Object visitLiteralExpr(const Literal& expr) override; -}; - -#endif diff --git a/src/cmd/callable.hpp b/src/cmd/callable.hpp new file mode 100644 index 0000000..081b40d --- /dev/null +++ b/src/cmd/callable.hpp @@ -0,0 +1,30 @@ +#ifndef CALLABLE_H +#define CALLABLE_H + +#include +#include +#include + +#include "object.hpp" + +class Interpreter; + +class Callable { + public: + virtual ~Callable() = default; + virtual Object call(std::vector args) = 0; + [[nodiscard]] virtual int arity() const = 0; + [[nodiscard]] virtual std::string str() const = 0; +}; + +template + requires std::derived_from +struct std::formatter { // NOLINT(cert-dcl58-cpp) + constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } + + auto format(const T& fn, std::format_context& ctx) const { + return std::format_to(ctx.out(), fn.str()); + } +}; + +#endif diff --git a/src/cmd/environment.cpp b/src/cmd/environment.cpp index e5506d6..6f4915f 100644 --- a/src/cmd/environment.cpp +++ b/src/cmd/environment.cpp @@ -1,26 +1,51 @@ #include "environment.hpp" #include +#include +#include +#include "error.hpp" #include "object.hpp" -#include "runtime_error.hpp" #include "token.hpp" void Environment::define(const std::string& name, Object value) { - m_values[name] = value; + if (m_values.contains(name)) + if (auto* ptr = std::get_if>(&m_values[name])) { + Error::error(TokenType::IDENTIFIER, + std::format("Cannot assign to {} as it is a function", name), + ErrorType::RUNTIME_ERROR); + return; + } + + m_values[name] = std::move(value); } -Object Environment::get(Token name) { +Object Environment::get(const Token& name) { if (m_values.contains(name.m_lexeme)) return m_values[name.m_lexeme]; - throw RuntimeError(std::format("Undefined variable '{}'.", name.m_lexeme)); + Error::error(name.m_type, + std::format("Undefined variable '{}'.", name.m_lexeme), + ErrorType::RUNTIME_ERROR); + return std::monostate{}; } -void Environment::assign(Token name, Object value) { +void Environment::assign(const Token& name, Object value) { if (m_values.contains(name.m_lexeme)) { - m_values[name.m_lexeme] = value; + if (auto* val = + std::get_if>(&m_values[name.m_lexeme]); + val != nullptr) { + Error::error( + name.m_type, + std::format("Cannot reassign {} as it is a function", name.m_lexeme), + ErrorType::RUNTIME_ERROR); + return; + } + m_values[name.m_lexeme] = std::move(value); + return; } - throw RuntimeError(std::format("Undefined variable '{}'", name.m_lexeme)); + Error::error(name.m_type, + std::format("Undefined variable '{}'", name.m_lexeme), + ErrorType::RUNTIME_ERROR); } Environment& Environment::getInstance() { diff --git a/src/cmd/environment.hpp b/src/cmd/environment.hpp index f1f7845..e27fd29 100644 --- a/src/cmd/environment.hpp +++ b/src/cmd/environment.hpp @@ -2,18 +2,25 @@ #define ENVIRONMENT_HPP #include +#include #include "object.hpp" +#include "stdlib.hpp" #include "token.hpp" + +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) class Environment { private: - std::map m_values = {}; - Environment() = default; + std::map m_values; + Environment() : m_values({}) { + this->define("print", std::make_shared(PrintFn())); + this->define("len", std::make_shared(LenFn())); + } public: void define(const std::string& name, Object value); - Object get(Token name); - void assign(Token name, Object value); + Object get(const Token& name); + void assign(const Token& name, Object value); Environment(Environment& other) = delete; Environment& operator=(const Environment& other) = delete; diff --git a/src/cmd/error.cpp b/src/cmd/error.cpp index ab3523a..3c070d5 100644 --- a/src/cmd/error.cpp +++ b/src/cmd/error.cpp @@ -2,33 +2,21 @@ #include -#include "runtime_error.hpp" +#include "formatter.hpp" -void Err::report(int line, const std::string& where, const std::string& msg) { - std::cout << std::format("[line {}] Error {}: {}\n", line, where, msg); - hadError = true; +void Error::_error(TokenType where, const std::string& msg, ErrorType type) { + std::cerr << std::format("{} error at {} : {}\n", type, where, msg); + had_error = true; } -void Err::error(const int line, const std::string& msg) { - report(line, "", msg); +void Error::error(TokenType where, const std::string& msg, ErrorType type) { + Error& e = Error::getInstance(); + e._error(where, msg, type); } -void Err::error(Token token, const std::string& msg) { - if (token.m_type == TokenType::END) { - report(token.m_line, " at end", msg); - } else { - report(token.m_line, std::format("at '{}'", token.m_lexeme), msg); - } -} - -void Err::runtimeError(RuntimeError& err) { - std::cerr << std::format("{}\n[line ????]", err.what()) << std::endl; - hadRuntimeError = true; -} - -Err::Err() : hadError(false), hadRuntimeError(false) {} +Error::Error() = default; -Err& Err::getInstance() { - static Err instance; +Error& Error::getInstance() { + static Error instance; return instance; } diff --git a/src/cmd/error.hpp b/src/cmd/error.hpp index 5d47d0d..7b79871 100644 --- a/src/cmd/error.hpp +++ b/src/cmd/error.hpp @@ -1,27 +1,48 @@ #ifndef ERROR_H #define ERROR_H +#include +#include +#include #include -#include "runtime_error.hpp" -#include "token.hpp" +enum class ErrorType : std::uint8_t { PARSE_ERROR, SCAN_ERROR, RUNTIME_ERROR }; +enum class TokenType : std::uint8_t; -class Err { +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) +class Error { private: - Err(); + Error(); + // NOLINTNEXTLINE(readability-identifier-naming) + void _error(TokenType where, const std::string& msg, ErrorType type); public: - Err(Err& other) = delete; - Err& operator=(const Err& other) = delete; - Err(Err&& other) = delete; - Err& operator=(Err&& other) = delete; - - bool hadError; - bool hadRuntimeError; - void report(int line, const std::string& where, const std::string& msg); - void error(int line, const std::string& msg); - void error(Token token, const std::string& msg); - void runtimeError(RuntimeError& err); - static Err& getInstance(); + Error(Error& other) = delete; + Error& operator=(const Error& other) = delete; + Error(Error&& other) = delete; + Error& operator=(Error&& other) = delete; + + bool had_error{false}; + static void error(TokenType where, const std::string& msg, ErrorType type); + static Error& getInstance(); +}; + +template <> +struct std::formatter : std::formatter { + constexpr auto format(ErrorType type, auto& ctx) const { + const auto str = [] { + std::map res; +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define INSERT_ELEM(p) res.emplace(p, #p); + INSERT_ELEM(ErrorType::PARSE_ERROR); + INSERT_ELEM(ErrorType::SCAN_ERROR); + INSERT_ELEM(ErrorType::RUNTIME_ERROR); +#undef INSERT_ELEM + + return res; + }; + + return std::formatter::format(str()[type], ctx); + }; }; #endif diff --git a/src/cmd/expr.hpp b/src/cmd/expr.hpp index 3f85627..173cbfa 100644 --- a/src/cmd/expr.hpp +++ b/src/cmd/expr.hpp @@ -5,6 +5,7 @@ #include #include +#include "formatter.hpp" #include "object.hpp" #include "token.hpp" @@ -25,15 +26,10 @@ class IExprVisitor { virtual ~IExprVisitor() = default; virtual Object visitBinaryExpr(const Binary& expr) = 0; - virtual Object visitGroupingExpr(const Grouping& expr) = 0; - virtual Object visitLiteralExpr(const Literal& expr) = 0; - virtual Object visitUnaryExpr(const Unary& expr) = 0; - virtual Object visitVariableExpr(const Variable& expr) = 0; - virtual Object visitAssignExpr(const Assign& expr) = 0; }; @@ -45,17 +41,21 @@ class Expr { }; template <> -struct std::formatter { - constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } +struct std::formatter { // NOLINT(cert-dcl58-cpp) + static constexpr auto parse(std::format_parse_context& ctx) { + return ctx.begin(); + } - auto format(const Expr& t, std::format_context& ctx) const { + static auto format(const Expr& t, std::format_context& ctx) { return std::format_to(ctx.out(), "{}", t.str()); } }; template <> -struct std::formatter { - constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } +struct std::formatter { // NOLINT(cert-dcl58-cpp) + static constexpr auto parse(std::format_parse_context& ctx) { + return ctx.begin(); + } auto format(Expr* expr, auto& ctx) const { if (expr) { @@ -66,6 +66,7 @@ struct std::formatter { }; template <> +// NOLINTNEXTLINE(cert-dcl58-cpp) struct std::formatter> : public std::formatter { auto format(const std::unique_ptr& expr, std::format_context& ctx) const { @@ -75,6 +76,7 @@ struct std::formatter> : public std::formatter { template requires std::derived_from && (!std::same_as) +// NOLINTNEXTLINE(cert-dcl58-cpp) struct std::formatter : std::formatter { auto format(const T& expr, std::format_context& ctx) const { return std::formatter::format(static_cast(expr), ctx); @@ -88,7 +90,7 @@ class Binary final : public Expr { m_op(std::move(op)), m_right(std::move(right)) {} - Object accept(IExprVisitor* visitor) const override { + [[nodiscard]] Object accept(IExprVisitor* visitor) const override { return visitor->visitBinaryExpr(*this); } @@ -152,7 +154,7 @@ class Variable final : public Expr { public: Token m_name; - explicit Variable(Token name) : m_name(name) {} + explicit Variable(Token name) : m_name(std::move(std::move(name))) {} Object accept(IExprVisitor* visitor) const override { return visitor->visitVariableExpr(*this); @@ -169,7 +171,7 @@ class Assign final : public Expr { std::unique_ptr m_value; explicit Assign(Token name, std::unique_ptr value) - : m_name(name), m_value(std::move(value)) {} + : m_name(std::move(std::move(name))), m_value(std::move(value)) {} Object accept(IExprVisitor* visitor) const override { return visitor->visitAssignExpr(*this); diff --git a/src/cmd/formatter.hpp b/src/cmd/formatter.hpp new file mode 100644 index 0000000..448dbf3 --- /dev/null +++ b/src/cmd/formatter.hpp @@ -0,0 +1,106 @@ +#ifndef FORMATTER_H +#define FORMATTER_H + +#include +#include +#include +#include +#include + +#include "callable.hpp" +#include "object.hpp" +#include "token.hpp" +#include "token_type.hpp" + +namespace std { +template <> +// NOLINTNEXTLINE(cert-dcl58-cpp) +struct formatter { + static constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } + + static auto format(const Object& obj, format_context& ctx) { + auto visitor = [&ctx](const auto& value) -> format_context::iterator { + using T = decay_t; + + if constexpr (is_same_v) { + return format_to(ctx.out(), "(null)"); + } else if constexpr (is_same_v) { + return format_to(ctx.out(), "{}", value ? "true" : "false"); + } else if constexpr (is_same_v) { + string text = std::format("{}", value); + + if (text.ends_with(".0")) { + text = text.substr(0, text.length() - 2); + } + + return format_to(ctx.out(), "{}", text); + + } else if constexpr (is_same_v>) { + return format_to(ctx.out(), "{}", value->str()); + } + + else { + return format_to(ctx.out(), "{}", value); + } + }; + + return visit(visitor, obj); + } +}; + +template <> +// NOLINTNEXTLINE(cert-dcl58-cpp) +struct formatter { + static constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } + + static auto format(const Token& t, format_context& ctx) { + return format_to(ctx.out(), "{}", t.toString()); + } +}; + +template <> +// NOLINTNEXTLINE(cert-dcl58-cpp) +struct formatter { + static constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } + + static auto format(const TokenType& t, format_context& ctx) { + const auto str = [] { + map res; +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define INSERT_ELEM(p) res.emplace(p, #p); + INSERT_ELEM(TokenType::LEFT_PAREN); + INSERT_ELEM(TokenType::RIGHT_PAREN); + INSERT_ELEM(TokenType::MINUS); + INSERT_ELEM(TokenType::PLUS); + INSERT_ELEM(TokenType::SLASH); + INSERT_ELEM(TokenType::STAR); + INSERT_ELEM(TokenType::BANG); + INSERT_ELEM(TokenType::BANG_EQUAL); + INSERT_ELEM(TokenType::EQUAL); + INSERT_ELEM(TokenType::EQUAL_EQUAL); + INSERT_ELEM(TokenType::GREATER); + INSERT_ELEM(TokenType::GREATER_EQUAL); + INSERT_ELEM(TokenType::LESS); + INSERT_ELEM(TokenType::LESS_EQUAL); + INSERT_ELEM(TokenType::IDENTIFIER); + INSERT_ELEM(TokenType::STRING); + INSERT_ELEM(TokenType::NUMBER); + INSERT_ELEM(TokenType::NIL); + INSERT_ELEM(TokenType::TRUE); + INSERT_ELEM(TokenType::FALSE); + INSERT_ELEM(TokenType::VAR); + INSERT_ELEM(TokenType::END); +#undef INSERT_ELEM + return res; + }; + + return format_to(ctx.out(), "{}", str()[t]); + } +}; +} // namespace std + +inline std::string Token::toString() const { + return std::format("{} {} {}", m_type, m_lexeme, m_literal); +} + +#endif diff --git a/src/cmd/interpreter.cpp b/src/cmd/interpreter.cpp index 4335aad..fc95101 100644 --- a/src/cmd/interpreter.cpp +++ b/src/cmd/interpreter.cpp @@ -1,12 +1,12 @@ #include "interpreter.hpp" -#include +#include #include +#include "callable.hpp" #include "error.hpp" #include "expr.hpp" #include "object.hpp" -#include "runtime_error.hpp" #include "stmnt.hpp" #include "token.hpp" @@ -38,7 +38,8 @@ void Interpreter::checkNumberOperand(const Token& op, const Object& operand) { if constexpr (std::is_same_v) return; - throw RuntimeError("Operand must be a number"); + Error::error(op.m_type, "Operand must be a number", + ErrorType::RUNTIME_ERROR); }; std::visit(visitor, operand); @@ -48,15 +49,8 @@ std::string Interpreter::stringify(const Object& object) { return std::format("{}", object); } -void Interpreter::interpret(const std::vector>& stmnts) { - try { - for (const auto& s : stmnts) { - execute(s); - } - } catch (RuntimeError& e) { - Err& err = Err::getInstance(); - err.runtimeError(e); - } +Object Interpreter::interpret(const std::unique_ptr& stmnt) { + return execute(stmnt); } Object Interpreter::visitLiteralExpr(const Literal& expr) { @@ -84,9 +78,9 @@ Object Interpreter::visitUnaryExpr(const Unary& expr) { return std::monostate{}; } -Object Interpreter::visitBinaryExpr(const Binary& expr) { - Object left = evaluate(expr.m_left); - Object right = evaluate(expr.m_right); +[[nodiscard]] Object Interpreter::visitBinaryExpr(const Binary& expr) { + const Object left = evaluate(expr.m_left); + const Object right = evaluate(expr.m_right); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wswitch" @@ -118,14 +112,32 @@ Object Interpreter::visitBinaryExpr(const Binary& expr) { } Object Interpreter::visitExprStmnt(const ExprStmnt& stmnt) { - evaluate(stmnt.m_expr); - return std::monostate{}; + return evaluate(stmnt.m_expr); } -Object Interpreter::visitPrintStmnt(const PrintStmnt& stmnt) { - Object value = evaluate(stmnt.m_expr); - std::cout << stringify(value) << std::endl; - return std::monostate{}; +Object Interpreter::visitCallStmnt(const CallStmnt& stmnt) { + // TODO: Use array instead? + std::vector argList = {}; + argList.reserve(stmnt.m_args.size()); + + for (const auto& x : stmnt.m_args) { + argList.emplace_back(std::move(evaluate(x))); + } + + Object fnObj = m_env.get(stmnt.m_fn); + auto* fn = std::get_if>(&fnObj); + + if (fn != nullptr) { + if (argList.size() != fn->get()->arity()) { + Error::error( + stmnt.m_fn.m_type, + std::format("Function requires {} arguments but {} were provided", + fn->get()->arity(), argList.size()), + ErrorType::RUNTIME_ERROR); + return std::monostate{}; + } + } + return fn->get()->call(std::move(argList)); } Object Interpreter::execute(const std::unique_ptr& stmnt) { diff --git a/src/cmd/interpreter.hpp b/src/cmd/interpreter.hpp index e870b6a..dfd35ba 100644 --- a/src/cmd/interpreter.hpp +++ b/src/cmd/interpreter.hpp @@ -2,6 +2,7 @@ #define INTERPRETER_HPP #include "environment.hpp" +#include "error.hpp" #include "expr.hpp" #include "object.hpp" #include "stmnt.hpp" @@ -9,12 +10,12 @@ class Interpreter : public IExprVisitor, IStmntVisitor { private: Environment& m_env = Environment::getInstance(); + Error& err = Error::getInstance(); Object evaluate(const std::unique_ptr& expr); - bool isTruthy(const Object& object); - bool isEqual(const Object& lhs, const Object& rhs); - void checkNumberOperand(const Token& op, const Object& operand); - std::string stringify(const Object& object); + static bool isTruthy(const Object& object); + static bool isEqual(const Object& lhs, const Object& rhs); + static void checkNumberOperand(const Token& op, const Object& operand); Object execute(const std::unique_ptr& stmnt); public: @@ -23,11 +24,12 @@ class Interpreter : public IExprVisitor, IStmntVisitor { Object visitUnaryExpr(const Unary& expr) override; Object visitBinaryExpr(const Binary& expr) override; Object visitVariableExpr(const Variable& expr) override; - void interpret(const std::vector>& stmnts); + Object interpret(const std::unique_ptr& stmnt); Object visitExprStmnt(const ExprStmnt& stmnt) override; - Object visitPrintStmnt(const PrintStmnt& stmnt) override; Object visitVarStmnt(const VarStmnt& stmnt) override; Object visitAssignExpr(const Assign& expr) override; + Object visitCallStmnt(const CallStmnt& stmnt) override; + static std::string stringify(const Object& object); }; #endif // !INTERPRETER_HPP diff --git a/src/cmd/object.hpp b/src/cmd/object.hpp index 81836f9..2e43d63 100644 --- a/src/cmd/object.hpp +++ b/src/cmd/object.hpp @@ -1,22 +1,26 @@ #ifndef OBJECT_H #define OBJECT_H -#include + +#include #include +#include #include #include #include #include -#include "runtime_error.hpp" +#include "error.hpp" +#include "token_type.hpp" + +class Callable; -using Object = - std::variant; +using Object = std::variant>; template -inline Object binary_operation(const Object& lhs, const Object& rhs, Op op, - const std::string& op_name) { - auto visitor = [&op, &op_name](const auto& left, - const auto& right) -> Object { +inline Object binaryOperation(const Object& lhs, const Object& rhs, Op op, + const std::string& opName) { + auto visitor = [&op, &opName](const auto& left, const auto& right) -> Object { using L = std::decay_t; using R = std::decay_t; @@ -27,12 +31,16 @@ inline Object binary_operation(const Object& lhs, const Object& rhs, Op op, if constexpr (std::is_same_v>) { return left + right; } else { - throw RuntimeError( - std::format("Cannot apply operator {} to strings", op_name)); + Error::error(TokenType::STRING, + std::format("Cannot apply operator {} to strings", opName), + ErrorType::RUNTIME_ERROR); + return std::monostate{}; } } else { - throw RuntimeError( - std::format("Unsupported operand types for {}", op_name)); + Error::error(TokenType::STRING, + std::format("Unsupported operand types for {}", opName), + ErrorType::RUNTIME_ERROR); + return std::monostate{}; } }; @@ -40,17 +48,21 @@ inline Object binary_operation(const Object& lhs, const Object& rhs, Op op, } template -inline bool comparison_operation(const Object& lhs, const Object& rhs, Op op, - const std::string& op_name) { - auto visitor = [&op, &op_name](const auto& left, const auto& right) -> bool { +inline bool comparisonOperation(const Object& lhs, const Object& rhs, Op op, + const std::string& opName) { + auto visitor = [&op, &opName](const auto& left, const auto& right) -> bool { using L = std::decay_t; using R = std::decay_t; if constexpr (std::is_arithmetic_v && std::is_arithmetic_v) { return op(static_cast(left), static_cast(right)); } else { - throw RuntimeError(std::format( - "Cannot apply operator {} to non-arithmetic types", op_name)); + Error::error( + TokenType::STRING, + std::format("Cannot apply operator {} to non-arithmetic types", + opName), + ErrorType::RUNTIME_ERROR); + return false; } }; @@ -67,90 +79,64 @@ inline Object operator+(const Object& lhs, const Object& rhs) { return left + right; } - return binary_operation(left, right, std::plus{}, "+"); + return binaryOperation(left, right, std::plus{}, "+"); }; return std::visit(visitor, lhs, rhs); } inline Object operator-(const Object& lhs, const Object& rhs) { - return binary_operation(lhs, rhs, std::minus{}, "-"); + return binaryOperation(lhs, rhs, std::minus{}, "-"); } inline Object operator/(const Object& lhs, const Object& rhs) { - return binary_operation(lhs, rhs, std::divides{}, "/"); + return binaryOperation(lhs, rhs, std::divides{}, "/"); } inline Object operator*(const Object& lhs, const Object& rhs) { - return binary_operation(lhs, rhs, std::multiplies{}, "*"); + return binaryOperation(lhs, rhs, std::multiplies{}, "*"); } inline bool operator==(const Object& lhs, const Object& rhs) { - return comparison_operation(lhs, rhs, std::equal_to{}, "=="); + if (lhs.index() == rhs.index()) { + return std::visit([](const auto& l, const auto& r) { return l == r; }, lhs, + rhs); + }; + + return false; } inline bool operator!=(const Object& lhs, const Object& rhs) { - return comparison_operation(lhs, rhs, std::not_equal_to{}, "!="); + return !(lhs == rhs); } inline bool operator<(const Object& lhs, const Object& rhs) { - return comparison_operation(lhs, rhs, std::less{}, "<"); + return comparisonOperation(lhs, rhs, std::less{}, "<"); } inline bool operator>(const Object& lhs, const Object& rhs) { - return comparison_operation(lhs, rhs, std::greater{}, ">"); + return comparisonOperation(lhs, rhs, std::greater{}, ">"); } inline bool operator<=(const Object& lhs, const Object& rhs) { - return comparison_operation(lhs, rhs, std::less_equal{}, "<="); + return comparisonOperation(lhs, rhs, std::less_equal{}, "<="); } inline bool operator>=(const Object& lhs, const Object& rhs) { - return comparison_operation(lhs, rhs, std::greater_equal{}, ">="); + return comparisonOperation(lhs, rhs, std::greater_equal{}, ">="); } namespace detail { template requires(std::is_arithmetic_v) -[[nodiscard]] std::string to_string(const T& val, const int p = 3) { +[[nodiscard]] std::string toString(const T& val, const int p = 3) { std::ostringstream out; out.precision(p); out << std::fixed << val; return std::move(out).str(); } -inline std::string bool_to_s(bool b) { return b ? "(true)" : "(false)"; } +inline std::string boolToS(bool b) { return b ? "(true)" : "(false)"; } } // namespace detail -template <> -struct std::formatter { - constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } - - auto format(const Object& obj, std::format_context& ctx) const { - auto visitor = [&ctx](const auto& value) -> std::format_context::iterator { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - return std::format_to(ctx.out(), "emtpy"); - } else if constexpr (std::is_same_v) { - return std::format_to(ctx.out(), "{}", value); - } else if constexpr (std::is_same_v) { - return std::format_to(ctx.out(), "{}", value ? "true" : "false"); - } else if constexpr (std::is_same_v) { - std::string text = std::format("{}", value); - - if (text.ends_with(".0")) { - text = text.substr(0, text.length() - 2); - } - - return std::format_to(ctx.out(), "{}", text); - } else { - return std::format_to(ctx.out(), "{}", value); - } - }; - - return std::visit(visitor, obj); - } -}; - #endif diff --git a/src/cmd/parse_error.hpp b/src/cmd/parse_error.hpp deleted file mode 100644 index be6b7e8..0000000 --- a/src/cmd/parse_error.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef PARSE_ERR_HPP -#define PARSE_ERR_HPP - -#include -#include - -class ParseError : public std::exception { - private: - std::string message; - - public: - inline ParseError() : message("Parsing error occured.") {} - inline ParseError(char* msg) : message(msg) {} - inline const char* what() { return message.c_str(); } -}; - -#endif // !PARSE_ERR_HPP diff --git a/src/cmd/parser.cpp b/src/cmd/parser.cpp index 613e43d..22c090a 100644 --- a/src/cmd/parser.cpp +++ b/src/cmd/parser.cpp @@ -5,7 +5,6 @@ #include "error.hpp" #include "expr.hpp" -#include "parse_error.hpp" #include "stmnt.hpp" #include "token.hpp" @@ -17,7 +16,7 @@ std::unique_ptr Parser::equality() { std::unique_ptr expr = comparison(); while (match(TokenType::BANG_EQUAL, TokenType::EQUAL_EQUAL)) { - Token op = previous(); + const Token op = previous(); std::unique_ptr right = comparison(); expr = std::make_unique(std::move(expr), op, std::move(right)); } @@ -25,10 +24,7 @@ std::unique_ptr Parser::equality() { return expr; } -bool Parser::check(TokenType type) { - if (isAtEnd()) return false; - return peek().m_type == type; -} +bool Parser::check(TokenType type) { return peek().m_type == type; } Token Parser::advance() { if (!isAtEnd()) m_current++; @@ -45,7 +41,7 @@ std::unique_ptr Parser::term() { std::unique_ptr expr = factor(); while (match(TokenType::MINUS, TokenType::PLUS)) { - Token op = previous(); + const Token op = previous(); std::unique_ptr right = factor(); expr = std::make_unique(std::move(expr), op, std::move(right)); } @@ -57,7 +53,7 @@ std::unique_ptr Parser::factor() { std::unique_ptr expr = unary(); while (match(TokenType::SLASH, TokenType::STAR)) { - Token op = previous(); + const Token op = previous(); std::unique_ptr right = unary(); expr = std::make_unique(std::move(expr), op, std::move(right)); } @@ -67,7 +63,7 @@ std::unique_ptr Parser::factor() { std::unique_ptr Parser::unary() { if (match(TokenType::BANG, TokenType::MINUS)) { - Token op = previous(); + const Token op = previous(); std::unique_ptr right = unary(); return std::make_unique(op, std::move(right)); } @@ -93,7 +89,8 @@ std::unique_ptr Parser::primary() { return std::make_unique(std::move(e)); } - throw error(peek(), "Expected expression."); + Error::error(peek().m_type, "Expected expression.", ErrorType::PARSE_ERROR); + return nullptr; } std::unique_ptr Parser::comparison() { @@ -101,7 +98,7 @@ std::unique_ptr Parser::comparison() { while (match(TokenType::GREATER, TokenType::GREATER_EQUAL, TokenType::LESS, TokenType::LESS_EQUAL)) { - Token op = previous(); + const Token op = previous(); std::unique_ptr right = term(); expr = std::make_unique(std::move(expr), op, std::move(right)); } @@ -112,85 +109,59 @@ std::unique_ptr Parser::comparison() { Token Parser::consume(TokenType type, const std::string& msg) { if (check(type)) return advance(); - throw error(peek(), msg); + Error::error(peek().m_type, msg, ErrorType::PARSE_ERROR); + return peek(); // Return current token on error } -ParseError Parser::error(Token token, const std::string& msg) { - Err& err = Err::getInstance(); - err.error(token, msg); - return ParseError(); -} +std::unique_ptr Parser::statement() { + if (check(TokenType::IDENTIFIER)) { + const int saved = m_current; + advance(); -void Parser::synchronise() { - advance(); - - while (!isAtEnd()) { - if (previous().m_type == TokenType::SEMICOLON) return; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wswitch" - switch (peek().m_type) { - case TokenType::CLASS: - case TokenType::FUN: - case TokenType::VAR: - case TokenType::FOR: - case TokenType::IF: - case TokenType::WHILE: - case TokenType::PRINT: - case TokenType::RETURN: - return; + // Assignment + if (check(TokenType::EQUAL)) { + m_current = saved; + return exprStmnt(); } -#pragma clang diagnostic pop - advance(); - } -} + // Function call + if (check(TokenType::IDENTIFIER) || check(TokenType::NUMBER) || + check(TokenType::STRING) || check(TokenType::LEFT_PAREN) || + check(TokenType::TRUE) || check(TokenType::FALSE) || + check(TokenType::NIL)) { + m_current = saved; + return funStmnt(); + } -std::unique_ptr Parser::statement() { - if (match(TokenType::PRINT)) return printStmnt(); + // Expression + m_current = saved; + } return exprStmnt(); } -std::unique_ptr Parser::printStmnt() { - std::unique_ptr value = expression(); - consume(TokenType::SEMICOLON, "Expect ';' after value."); - return std::make_unique(std::move(value)); -} - std::unique_ptr Parser::exprStmnt() { std::unique_ptr expr = expression(); - consume(TokenType::SEMICOLON, "Expect ';' after expression."); + if (!expr) return nullptr; // Don't continue if expression failed + consume(TokenType::END, "Expect EOF after expression."); return std::make_unique(std::move(expr)); } -std::vector> Parser::parse() { - std::vector> statements; - - while (!isAtEnd()) { - statements.emplace_back(declaration()); - } - - return statements; -} +std::unique_ptr Parser::parse() { return declaration(); } std::unique_ptr Parser::declaration() { - try { - if (match(TokenType::VAR)) return varDeclaration(); - - return statement(); - } catch (ParseError& err) { - synchronise(); - // TODO: Maybe this isn't the _best_ idea - return nullptr; - } + if (match(TokenType::VAR)) return varDeclaration(); + return statement(); } std::unique_ptr Parser::varDeclaration() { - Token name = consume(TokenType::IDENTIFIER, "Expect variable name"); + const Token name = consume(TokenType::IDENTIFIER, "Expect variable name"); std::unique_ptr initialiser = {}; - if (match(TokenType::EQUAL)) initialiser = expression(); - consume(TokenType::SEMICOLON, "Expect ';' after variable declaration"); + if (match(TokenType::EQUAL)) { + initialiser = expression(); + if (!initialiser) return nullptr; // Don't continue if expression failed + } + consume(TokenType::END, "Expect EOF after variable declaration"); return std::make_unique(name, std::move(initialiser)); } @@ -198,16 +169,32 @@ std::unique_ptr Parser::assignment() { std::unique_ptr expr = equality(); if (match(TokenType::EQUAL)) { - Token equals = previous(); + const Token equals = previous(); std::unique_ptr value = assignment(); - if (auto* ptr = dynamic_cast(value.get())) { - Token name = ptr->m_name; + if (auto* ptr = dynamic_cast(expr.get())) { + const Token name = ptr->m_name; return std::make_unique(name, std::move(value)); } - error(equals, "Invalid assignment target."); + Error::error(equals.m_type, "Invalid assignment target.", + ErrorType::PARSE_ERROR); } return expr; } + +std::unique_ptr Parser::funStmnt() { + const Token fnName = consume(TokenType::IDENTIFIER, "Expect function name"); + + std::vector> args = {}; + + while (!check(TokenType::END)) { + auto arg = expression(); + if (!arg) return nullptr; // Don't continue if expression failed + args.emplace_back(std::move(arg)); + } + + consume(TokenType::END, "Expect EOF after function call."); + return std::make_unique(fnName, std::move(args)); +} diff --git a/src/cmd/parser.hpp b/src/cmd/parser.hpp index a06698a..2f4b1b0 100644 --- a/src/cmd/parser.hpp +++ b/src/cmd/parser.hpp @@ -5,9 +5,9 @@ #include #include "expr.hpp" -#include "parse_error.hpp" #include "stmnt.hpp" #include "token.hpp" + class Parser { private: std::vector m_tokens; @@ -31,18 +31,21 @@ class Parser { std::unique_ptr primary(); std::unique_ptr comparison(); Token consume(TokenType type, const std::string& msg); - ParseError error(Token token, const std::string& msg); - void synchronise(); + template + requires(std::is_same_v && ...) + Token consumeAnyOf(Args... tokens, const std::string& msg) { + return (... || consume(tokens, msg)); + } std::unique_ptr statement(); - std::unique_ptr printStmnt(); std::unique_ptr exprStmnt(); std::unique_ptr declaration(); std::unique_ptr varDeclaration(); std::unique_ptr assignment(); + std::unique_ptr funStmnt(); public: explicit Parser(std::vector tokens); - std::vector> parse(); + std::unique_ptr parse(); }; #endif // !PARSER_HPP diff --git a/src/cmd/process.cpp b/src/cmd/process.cpp deleted file mode 100644 index f63b79d..0000000 --- a/src/cmd/process.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "process.h" - -#include -#include -#include - -#include -#include -#include - -Process::Process(pid_t pid) : _pid(pid) {} -Process::~Process() { - std::cout << "Detaching\n"; - ptrace(PTRACE_DETACH, this->_pid, 0, 0); -} - -void Process::wait_status() const { - if (WIFSTOPPED(this->status)) - std::cout << fmt::format("Child stopped {}\n", WSTOPSIG(this->status)); - if (WIFEXITED(this->status)) - std::cout << fmt::format("Child exited {}\n", WEXITSTATUS(this->status)); - if (WIFSIGNALED(this->status)) - std::cout << fmt::format("Child signaled {}\n", WTERMSIG(this->status)); - if (WCOREDUMP(this->status)) std::cout << fmt::format("Child core dumped\n"); -} - -[[nodiscard]] const pid_t& Process::pid() { return this->_pid; } -[[nodiscard]] const user_regs_struct& Process::regs() { - if (ptrace(PTRACE_GETREGS, this->_pid, NULL, &this->_regs)) { - std::cerr << fmt::format("Error fetching registers from child {}\n", - strerror(errno)); - std::exit(-1); - } - return this->_regs; -} - -[[nodiscard]] int Process::sstep() { - int retval = ptrace(PTRACE_SINGLESTEP, this->_pid, 0, 0); - if (retval) return retval; - waitpid(this->_pid, &this->status, 0); - return this->status; -} diff --git a/src/cmd/process.h b/src/cmd/process.h deleted file mode 100644 index 4d4bc48..0000000 --- a/src/cmd/process.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef CAESAR_PROCESS_H -#define CAESAR_PROCESS_H - -#include -#include -class Process { -private: - pid_t _pid; - struct user_regs_struct _regs; - -public: - int status; - explicit Process(pid_t pid); - ~Process(); - - [[nodiscard]] const user_regs_struct ®s(); - [[nodiscard]] const pid_t &pid(); - void wait_status() const; - [[nodiscard]] int sstep(); -}; - -#endif // !CAESAR_PROCESS_H diff --git a/src/cmd/runtime_error.hpp b/src/cmd/runtime_error.hpp deleted file mode 100644 index 73fc5a1..0000000 --- a/src/cmd/runtime_error.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef RUNTIME_ERROR_HPP -#define RUNTIME_ERROR_HPP - -#include -#include - -class RuntimeError : public std::exception { - private: - std::string message; - - public: - inline RuntimeError() : message("Runtime error occured.") {} - inline RuntimeError(const std::string& msg) : message(msg) {} - inline const char* what() { return message.c_str(); } -}; - -#endif // !RUNTIME_ERROR_HPP diff --git a/src/cmd/scanner.cpp b/src/cmd/scanner.cpp index 438d5f1..b9edbdf 100644 --- a/src/cmd/scanner.cpp +++ b/src/cmd/scanner.cpp @@ -1,12 +1,14 @@ #include "scanner.hpp" +#include #include #include #include "error.hpp" +#include "token.hpp" #include "util.hpp" -Scanner::Scanner(std::string source) : m_source(std::move(source)) {} +Scanner::Scanner(std::string source) noexcept : m_source(std::move(source)) {} std::vector Scanner::scanTokens() { while (!isAtEnd()) { @@ -14,14 +16,14 @@ std::vector Scanner::scanTokens() { scanToken(); } - m_tokens.emplace_back(TokenType::END, "", std::monostate{}, m_line); + m_tokens.emplace_back(TokenType::END, "", std::monostate{}); return m_tokens; } bool Scanner::isAtEnd() const { return m_current >= m_source.length(); } void Scanner::scanToken() { - char c = advance(); + const char c = advance(); switch (c) { case '(': addToken(TokenType::LEFT_PAREN); @@ -29,27 +31,12 @@ void Scanner::scanToken() { case ')': addToken(TokenType::RIGHT_PAREN); break; - case '{': - addToken(TokenType::LEFT_BRACE); - break; - case '}': - addToken(TokenType::RIGHT_BRACE); - break; - case ',': - addToken(TokenType::COMMA); - break; - case '.': - addToken(TokenType::DOT); - break; case '-': addToken(TokenType::MINUS); break; case '+': addToken(TokenType::PLUS); break; - case ';': - addToken(TokenType::SEMICOLON); - break; case '*': addToken(TokenType::STAR); break; @@ -66,31 +53,23 @@ void Scanner::scanToken() { addToken(match('=') ? TokenType::GREATER_EQUAL : TokenType::GREATER); break; case '/': - if (match('/')) { - while (peek() != '\n' && !isAtEnd()) { - advance(); - } - } else { - addToken(TokenType::SLASH); - } + addToken(TokenType::SLASH); break; case ' ': case '\r': case '\t': break; - case '\n': - m_line++; - break; case '"': string(); break; default: - if (isdigit(c)) { + if (isdigit(c) != 0) { number(); - } else if (isalpha(c)) { + } else if (isalpha(c) != 0) { identifier(); } else { - e.error(m_line, "Unexpected character"); + Error::error(TokenType::END, "Unexpected character", + ErrorType::SCAN_ERROR); } break; } @@ -117,14 +96,12 @@ char Scanner::peek() const { void Scanner::string() { while (peek() != '"' && !isAtEnd()) { - if (peek() == '\n') { - m_line++; - } advance(); } if (isAtEnd()) { - e.error(m_line, "Unterminated string"); + Error::error(TokenType::STRING, "Unterminated string", + ErrorType::SCAN_ERROR); return; } @@ -135,26 +112,22 @@ void Scanner::string() { } void Scanner::number() { - while (isdigit(peek())) { + while (isdigit(peek()) != 0) { advance(); } - if (peek() == '.' && isdigit(peekNext())) { + if (peek() == '.' && (isdigit(peekNext()) != 0)) { advance(); - while (isdigit(peek())) { + while (isdigit(peek()) != 0) { advance(); } } - std::string text = m_source.substr(m_start, m_current - m_start); + const std::string text = m_source.substr(m_start, m_current - m_start); auto value = detail::parseNumber(text); - auto add_token = [this](T&& arg) { - addToken(TokenType::NUMBER, std::move(arg)); - }; - - std::visit(add_token, value); + addToken(TokenType::NUMBER, value); } char Scanner::peekNext() const { @@ -165,10 +138,10 @@ char Scanner::peekNext() const { } void Scanner::identifier() { - while (isalnum(peek())) advance(); + while (isalnum(peek()) != 0) advance(); const std::string text = m_source.substr(m_start, m_current - m_start); - TokenType type; + TokenType type{}; try { type = m_keywords.at(text); } catch (std::out_of_range& e) { diff --git a/src/cmd/scanner.hpp b/src/cmd/scanner.hpp index f942a50..d85d580 100644 --- a/src/cmd/scanner.hpp +++ b/src/cmd/scanner.hpp @@ -9,22 +9,14 @@ class Scanner { private: - Err& e = Err::getInstance(); const std::string m_source; std::vector m_tokens; int m_start = 0; int m_current = 0; - int m_line = 1; - std::map m_keywords = { - {"and", TokenType::AND}, {"class", TokenType::CLASS}, - {"else", TokenType::ELSE}, {"false", TokenType::FALSE}, - {"for", TokenType::FOR}, {"fun", TokenType::FUN}, - {"if", TokenType::IF}, {"nil", TokenType::NIL}, - {"or", TokenType::OR}, {"print", TokenType::PRINT}, - {"return", TokenType::RETURN}, {"super", TokenType::SUPER}, - {"this", TokenType::THIS}, {"true", TokenType::TRUE}, - {"var", TokenType::VAR}, {"while", TokenType::WHILE}, - }; + std::map m_keywords = {{"false", TokenType::FALSE}, + {"nil", TokenType::NIL}, + {"true", TokenType::TRUE}, + {"var", TokenType::VAR}}; [[nodiscard]] bool isAtEnd() const; void scanToken(); @@ -34,7 +26,7 @@ class Scanner { requires std::constructible_from void addToken(TokenType tokenType, const T& literal) { const std::string text = m_source.substr(m_start, m_current - m_start); - m_tokens.emplace_back(tokenType, text, literal, m_line); + m_tokens.emplace_back(tokenType, text, literal); } bool match(char expected); [[nodiscard]] char peek() const; @@ -44,7 +36,7 @@ class Scanner { void identifier(); public: - explicit Scanner(std::string source); + explicit Scanner(std::string source) noexcept; std::vector scanTokens(); }; diff --git a/src/cmd/stdlib.hpp b/src/cmd/stdlib.hpp new file mode 100644 index 0000000..96e4779 --- /dev/null +++ b/src/cmd/stdlib.hpp @@ -0,0 +1,39 @@ +#ifndef STDLIB_H +#define STDLIB_H + +#include + +#include "error.hpp" +#include "formatter.hpp" + +class LenFn : public Callable { + public: + [[nodiscard]] int arity() const override { return 1; } + [[nodiscard]] std::string str() const override { return ""; } + + Object call(std::vector args) override { + auto* val = std::get_if(args.data()); + + if (val == nullptr) { + Error::error(TokenType::IDENTIFIER, "len can only be called on a string", + ErrorType::RUNTIME_ERROR); + return std::monostate{}; + } + + return static_cast(val->length()); + } +}; + +class PrintFn : public Callable { + public: + [[nodiscard]] int arity() const override { return 1; } + [[nodiscard]] std::string str() const override { + return ""; + } + + Object call(std::vector args) override { + return std::format("{}", args[0]); + } +}; + +#endif diff --git a/src/cmd/stmnt.hpp b/src/cmd/stmnt.hpp index 96e8874..818d11c 100644 --- a/src/cmd/stmnt.hpp +++ b/src/cmd/stmnt.hpp @@ -2,22 +2,23 @@ #define STMNT_HPP #include +#include #include "expr.hpp" #include "object.hpp" #include "token.hpp" class ExprStmnt; -class PrintStmnt; class VarStmnt; +class CallStmnt; class IStmntVisitor { public: virtual ~IStmntVisitor() = default; virtual Object visitExprStmnt(const ExprStmnt& stmnt) = 0; - virtual Object visitPrintStmnt(const PrintStmnt& stmnt) = 0; virtual Object visitVarStmnt(const VarStmnt& stmnt) = 0; + virtual Object visitCallStmnt(const CallStmnt& stmnt) = 0; }; class Stmnt { @@ -30,21 +31,23 @@ class ExprStmnt final : public Stmnt { public: std::unique_ptr m_expr; - ExprStmnt(std::unique_ptr expr) : m_expr(std::move(expr)) {} + explicit ExprStmnt(std::unique_ptr expr) : m_expr(std::move(expr)) {} Object accept(IStmntVisitor* visitor) override { return visitor->visitExprStmnt(*this); } }; -class PrintStmnt final : public Stmnt { +class CallStmnt final : public Stmnt { public: - std::unique_ptr m_expr; + Token m_fn; + std::vector> m_args; - PrintStmnt(std::unique_ptr expr) : m_expr(std::move(expr)) {} + CallStmnt(Token fn, std::vector> expr) + : m_fn(std::move(fn)), m_args(std::move(expr)) {} Object accept(IStmntVisitor* visitor) override { - return visitor->visitPrintStmnt(*this); + return visitor->visitCallStmnt(*this); } }; @@ -54,7 +57,7 @@ class VarStmnt final : public Stmnt { Token m_name; VarStmnt(Token name, std::unique_ptr expr) - : m_initialiser(std::move(expr)), m_name(name) {} + : m_initialiser(std::move(expr)), m_name(std::move(std::move(name))) {} Object accept(IStmntVisitor* visitor) override { return visitor->visitVarStmnt(*this); diff --git a/src/cmd/token.hpp b/src/cmd/token.hpp index 658523b..22fba1d 100644 --- a/src/cmd/token.hpp +++ b/src/cmd/token.hpp @@ -1,132 +1,23 @@ #ifndef TOKEN_H #define TOKEN_H -#include -#include #include #include "object.hpp" - -enum class TokenType { - LEFT_PAREN, - RIGHT_PAREN, - LEFT_BRACE, - RIGHT_BRACE, - COMMA, - DOT, - MINUS, - PLUS, - SEMICOLON, - SLASH, - STAR, - BANG, - BANG_EQUAL, - EQUAL, - EQUAL_EQUAL, - GREATER, - GREATER_EQUAL, - LESS, - LESS_EQUAL, - IDENTIFIER, - STRING, - NUMBER, - AND, - CLASS, - ELSE, - FALSE, - FUN, - FOR, - IF, - NIL, - OR, - PRINT, - RETURN, - SUPER, - THIS, - TRUE, - VAR, - WHILE, - END -}; - -template <> -struct std::formatter : std::formatter { - auto format(const TokenType& t, auto& ctx) const { - const auto strings = [] { - std::map res; -#define INSERT_ELEM(p) res.emplace(p, #p); - INSERT_ELEM(TokenType::LEFT_PAREN); - INSERT_ELEM(TokenType::RIGHT_PAREN); - INSERT_ELEM(TokenType::LEFT_BRACE); - INSERT_ELEM(TokenType::RIGHT_BRACE); - INSERT_ELEM(TokenType::COMMA); - INSERT_ELEM(TokenType::DOT); - INSERT_ELEM(TokenType::MINUS); - INSERT_ELEM(TokenType::PLUS); - INSERT_ELEM(TokenType::SEMICOLON); - INSERT_ELEM(TokenType::SLASH); - INSERT_ELEM(TokenType::STAR); - INSERT_ELEM(TokenType::BANG); - INSERT_ELEM(TokenType::BANG_EQUAL); - INSERT_ELEM(TokenType::EQUAL); - INSERT_ELEM(TokenType::EQUAL_EQUAL); - INSERT_ELEM(TokenType::GREATER); - INSERT_ELEM(TokenType::GREATER_EQUAL); - INSERT_ELEM(TokenType::LESS); - INSERT_ELEM(TokenType::LESS_EQUAL); - INSERT_ELEM(TokenType::IDENTIFIER); - INSERT_ELEM(TokenType::STRING); - INSERT_ELEM(TokenType::NUMBER); - INSERT_ELEM(TokenType::AND); - INSERT_ELEM(TokenType::CLASS); - INSERT_ELEM(TokenType::ELSE); - INSERT_ELEM(TokenType::FALSE); - INSERT_ELEM(TokenType::FUN); - INSERT_ELEM(TokenType::FOR); - INSERT_ELEM(TokenType::IF); - INSERT_ELEM(TokenType::NIL); - INSERT_ELEM(TokenType::OR); - INSERT_ELEM(TokenType::PRINT); - INSERT_ELEM(TokenType::RETURN); - INSERT_ELEM(TokenType::SUPER) - INSERT_ELEM(TokenType::THIS); - INSERT_ELEM(TokenType::TRUE); - INSERT_ELEM(TokenType::VAR); - INSERT_ELEM(TokenType::WHILE); - INSERT_ELEM(TokenType::END); -#undef INSERT_ELEM - return res; - }; - - return std::format_to(ctx.out(), "{}", strings()[t]); - } -}; +#include "token_type.hpp" class Token { public: const TokenType m_type; const std::string m_lexeme; const Object m_literal; - const int m_line; template requires std::constructible_from - Token(TokenType type, std::string lexeme, T&& literal, int line) + Token(TokenType type, std::string lexeme, T&& literal) : m_type(type), m_lexeme(std::move(lexeme)), - m_literal(std::forward(literal)), - m_line(line){}; - [[nodiscard]] constexpr std::string toString() const { - return std::format("{} {} {}", m_type, m_lexeme, m_literal); - } -}; - -template <> -struct std::formatter { - constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } - - auto format(const Token& t, std::format_context& ctx) const { - return std::format_to(ctx.out(), "{}", t.toString()); - } + m_literal(std::forward(literal)) {} + [[nodiscard]] std::string toString() const; }; #endif diff --git a/src/cmd/token_type.hpp b/src/cmd/token_type.hpp new file mode 100644 index 0000000..be1cad2 --- /dev/null +++ b/src/cmd/token_type.hpp @@ -0,0 +1,31 @@ +#ifndef CAESAR_TOKEN_TYPE_H +#define CAESAR_TOKEN_TYPE_H + +#include + +enum class TokenType : std::uint8_t { + LEFT_PAREN, + RIGHT_PAREN, + MINUS, + PLUS, + SLASH, + STAR, + BANG, + BANG_EQUAL, + EQUAL, + EQUAL_EQUAL, + GREATER, + GREATER_EQUAL, + LESS, + LESS_EQUAL, + IDENTIFIER, + STRING, + NUMBER, + NIL, + TRUE, + FALSE, + VAR, + END, +}; + +#endif diff --git a/src/cmd/util.cpp b/src/cmd/util.cpp index 0ed1250..27f4aae 100644 --- a/src/cmd/util.cpp +++ b/src/cmd/util.cpp @@ -3,8 +3,7 @@ #include bool detail::isop(const char c) { - if (c == '+' || c == '-' || c == '*' || c == '/') return true; - return false; + return c == '+' || c == '-' || c == '*' || c == '/'; } TokenType detail::ctoop(const char c) { @@ -22,10 +21,10 @@ TokenType detail::ctoop(const char c) { } } -std::variant detail::parseNumber(const std::string& str) { +double detail::parseNumber(const std::string& str) { try { - size_t pos = 0; - int val = std::stoi(str, &pos); + size_t pos = 0; // NOLINT(misc-const-correctness) + const int val = std::stoi(str, &pos); if (pos == str.length() && str.find('.') == std::string::npos) { return val; } @@ -34,13 +33,13 @@ std::variant detail::parseNumber(const std::string& str) { } try { - float f_val = std::stof(str); - double d_val = std::stod(str); + const float fVal = std::stof(str); + const double dVal = std::stod(str); - if (std::abs(f_val - d_val) < 1e-16) { - return f_val; + if (std::abs(fVal - dVal) < 1e-16) { + return fVal; } - return d_val; + return dVal; } catch (...) { throw std::invalid_argument( std::format("Cannot parse {} as number\n", str)); diff --git a/src/cmd/util.hpp b/src/cmd/util.hpp index ece1916..eb84093 100644 --- a/src/cmd/util.hpp +++ b/src/cmd/util.hpp @@ -1,11 +1,12 @@ #ifndef UTIL_H #define UTIL_H + #include "token.hpp" namespace detail { bool isop(char c); TokenType ctoop(char c); -std::variant parseNumber(const std::string& str); +double parseNumber(const std::string& str); } // namespace detail #endif diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..005d90f --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(caesar_core STATIC + util.hpp + dwarf/context.cpp + dwarf/context.hpp + dwarf/alloc.hpp +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + target_sources(caesar_core PRIVATE + macho.hpp + macho.cpp + ) +endif() + +target_include_directories(caesar_core PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +find_package(libdwarf CONFIG REQUIRED) +target_link_libraries(caesar_core PUBLIC libdwarf::dwarf) diff --git a/src/main.cpp b/src/main.cpp index 58eef83..20ab263 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,48 +1,78 @@ +#include +#include +#include #include #include +#include #include +#include +#include +#include #include "cmd/error.hpp" #include "cmd/interpreter.hpp" #include "cmd/parser.hpp" #include "cmd/scanner.hpp" #include "cmd/token.hpp" +#include "object.hpp" +#include "stmnt.hpp" -Err& e = Err::getInstance(); +namespace { +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +Error& e = Error::getInstance(); void run(const std::string& src) { auto s = Scanner(src); const std::vector tokens = s.scanTokens(); - Parser p = Parser(tokens); - std::vector> statements = p.parse(); + if (e.had_error) { + return; // Stop if scan errors occurred + } - if (e.hadError) return; + Parser p = Parser(tokens); + std::unique_ptr const statement = p.parse(); + if (e.had_error) { + return; + } Interpreter interpreter = Interpreter(); - interpreter.interpret(statements); + Object const result = interpreter.interpret(statement); + if (e.had_error) { + return; + } + if (!std::holds_alternative(result)) { + std::cout << Interpreter::stringify(result) << '\n'; + } } void runFile(const std::string& path) { - std::ifstream file(path); + std::ifstream const file(path); std::stringstream buffer; buffer << file.rdbuf(); run(buffer.str()); - if (e.hadError) exit(65); - if (e.hadRuntimeError) exit(70); + if (e.had_error) { + exit(65); // NOLINT(concurrency-mt-unsafe) + } } void runPrompt() { std::string line; - for (;;) { + while (true) { line.clear(); std::cout << "> "; - std::getline(std::cin, line); - if (line.empty()) break; + std::cout.flush(); + if (!std::getline(std::cin, line)) { + break; + } + if (line.empty()) { + continue; + } run(line); - e.hadError = false; + e.had_error = false; } + std::cout << '\n'; } +} // namespace int main(int argc, char** argv) { try { diff --git a/src/syscall_decode.h b/src/syscall_decode.h index 08cd3e9..7703a02 100644 --- a/src/syscall_decode.h +++ b/src/syscall_decode.h @@ -1,19 +1,20 @@ #ifndef CAESAR_SYSCALL_DECODE_H #define CAESAR_SYSCALL_DECODE_H -#include #include +#include + class SyscallDecoder { -private: + private: // autogen this with m4, read from unistd_64.h for x86_64 // static constexpr std::map syscall_names = // std::map(); -public: + public: // provide human-readable syscall errors - static std::string syscall_err(const struct user_regs_struct ®s); - static constexpr std::string_view - syscall_name(const struct user_regs_struct ®s); + static std::string syscall_err(const struct user_regs_struct& regs); + static constexpr std::string_view syscall_name( + const struct user_regs_struct& regs); }; -#endif // !CAESAR_SYSCALL_DECODE_H +#endif // !CAESAR_SYSCALL_DECODE_H diff --git a/test/cmd/test_environment.cpp b/test/cmd/test_environment.cpp new file mode 100644 index 0000000..5dcf732 --- /dev/null +++ b/test/cmd/test_environment.cpp @@ -0,0 +1,170 @@ +#include +#include +#include +#include + +#include "environment.hpp" +#include "object.hpp" +#include "test_helpers.hpp" + +TEST_CASE("Test environment singleton pattern", "[environment][singleton]") { + Environment& env1 = Environment::getInstance(); + Environment& env2 = Environment::getInstance(); + REQUIRE(&env1 == &env2); +} + +TEST_CASE("Test environment define with different types", + "[environment][define]") { + Environment& env = Environment::getInstance(); + + auto [var_name, value, type_name] = + GENERATE(table({ + {"test_var_double", Object{42.0}, "double"}, + {"test_var_bool", Object{true}, "bool"}, + {"test_var_string", Object{"hello"}, "string"}, + {"test_var_monostate", Object{std::monostate{}}, "monostate"}, + })); + + DYNAMIC_SECTION(std::format("Define and get {} variable", type_name)) { + env.define(var_name, value); + Object retrieved = env.get(helpers::makeToken(var_name)); + + if (type_name == "double") { + REQUIRE(std::holds_alternative(retrieved)); + REQUIRE(std::get(retrieved) == std::get(value)); + } else if (type_name == "bool") { + REQUIRE(std::holds_alternative(retrieved)); + REQUIRE(std::get(retrieved) == std::get(value)); + } else if (type_name == "string") { + REQUIRE(std::holds_alternative(retrieved)); + REQUIRE(std::get(retrieved) == std::get(value)); + } else if (type_name == "monostate") { + REQUIRE(std::holds_alternative(retrieved)); + } + } +} + +TEST_CASE("Test environment define can update existing variable", + "[environment][define]") { + Environment& env = Environment::getInstance(); + + env.define("test_update_var", Object{10.0}); + Object first = env.get(helpers::makeToken("test_update_var")); + REQUIRE(std::holds_alternative(first)); + REQUIRE(std::get(first) == 10.0); + + env.define("test_update_var", Object{20.0}); + Object second = env.get(helpers::makeToken("test_update_var")); + REQUIRE(std::holds_alternative(second)); + REQUIRE(std::get(second) == 20.0); +} + +TEST_CASE("Test environment define cannot redefine built-in function", + "[environment][define][error]") { + Environment& env = Environment::getInstance(); + + auto fn_name = GENERATE(as{}, "print", "len"); + + DYNAMIC_SECTION(std::format("Cannot redefine {}", fn_name)) { + auto captured = helpers::captureStream( + std::cerr, [&env, &fn_name]() { env.define(fn_name, Object{42.0}); }); + + REQUIRE(captured.find(std::format("Cannot assign to {} as it is a function", + fn_name)) != std::string::npos); + } +} + +TEST_CASE("Test environment get returns correct values", "[environment][get]") { + Environment& env = Environment::getInstance(); + + env.define("test_get_var", Object{3.14}); + Object retrieved = env.get(helpers::makeToken("test_get_var")); + + REQUIRE(std::holds_alternative(retrieved)); + REQUIRE(std::get(retrieved) == 3.14); +} + +TEST_CASE("Test Environment get errors on undefined variable", + "[environment][get][error]") { + Environment& env = Environment::getInstance(); + + auto captured = helpers::captureStream(std::cerr, [&env]() { + env.get(helpers::makeToken("nonexistent_variable_xyz")); + }); + + REQUIRE(captured.find("Undefined variable 'nonexistent_variable_xyz'") != + std::string::npos); +} + +TEST_CASE("Test environment assign updates existing variable", + "[environment][assign]") { + Environment& env = Environment::getInstance(); + + env.define("test_assign_var", Object{100.0}); + + SECTION("Assign new value of same type") { + env.assign(helpers::makeToken("test_assign_var"), Object{200.0}); + Object result = env.get(helpers::makeToken("test_assign_var")); + REQUIRE(std::holds_alternative(result)); + REQUIRE(std::get(result) == 200.0); + } + + SECTION("Assign value of different type") { + env.assign(helpers::makeToken("test_assign_var"), Object{"hello"}); + Object result = env.get(helpers::makeToken("test_assign_var")); + REQUIRE(std::holds_alternative(result)); + REQUIRE(std::get(result) == "hello"); + } +} + +TEST_CASE("Test environment assign errors on undefined variable", + "[environment][assign][error]") { + Environment& env = Environment::getInstance(); + + auto captured = helpers::captureStream(std::cerr, [&env]() { + env.assign(helpers::makeToken("undefined_var_abc"), Object{42.0}); + }); + + REQUIRE(captured.find("Undefined variable 'undefined_var_abc'") != + std::string::npos); +} + +TEST_CASE("Test environment assign cannot reassign built-in function", + "[environment][assign][error]") { + Environment& env = Environment::getInstance(); + + auto fn_name = GENERATE(as{}, "print", "len"); + + DYNAMIC_SECTION(std::format("Cannot reassign {}", fn_name)) { + auto captured = helpers::captureStream(std::cerr, [&env, &fn_name]() { + env.assign(helpers::makeToken(fn_name), Object{42.0}); + }); + + REQUIRE(captured.find(std::format("Cannot reassign {} as it is a function", + fn_name)) != std::string::npos); + } +} + +TEST_CASE("Test environment type transitions", "[environment][types]") { + Environment& env = Environment::getInstance(); + + env.define("test_type_var", Object{5.0}); + + SECTION("double -> bool") { + env.assign(helpers::makeToken("test_type_var"), Object{true}); + Object result = env.get(helpers::makeToken("test_type_var")); + REQUIRE(std::holds_alternative(result)); + } + + SECTION("double -> string") { + env.assign(helpers::makeToken("test_type_var"), Object{"test"}); + Object result = env.get(helpers::makeToken("test_type_var")); + REQUIRE(std::holds_alternative(result)); + } + + SECTION("double -> monostate") { + env.assign(helpers::makeToken("test_type_var"), Object{std::monostate{}}); + Object result = env.get(helpers::makeToken("test_type_var")); + REQUIRE(std::holds_alternative(result)); + } +} diff --git a/test/cmd/test_helpers.hpp b/test/cmd/test_helpers.hpp new file mode 100644 index 0000000..8ba3b04 --- /dev/null +++ b/test/cmd/test_helpers.hpp @@ -0,0 +1,94 @@ +#ifndef CAESAR_TEST_HELPERS_HPP +#define CAESAR_TEST_HELPERS_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include "expr.hpp" +#include "parser.hpp" +#include "scanner.hpp" +#include "stmnt.hpp" +#include "token.hpp" +namespace helpers { +// Pass here expected amount of tokens WITHOUT end token +inline bool checkTokensSize(const size_t actual, const size_t expected) { + return actual == expected + 1; +} + +inline std::vector scan(const std::string& src) { + Scanner s(src); + auto tokens = s.scanTokens(); + REQUIRE(!tokens.empty()); + REQUIRE(tokens.back().m_type == TokenType::END); + return tokens; +} + +inline std::unique_ptr getStmnt(const std::string& input) { + auto tokens = scan(input); + Parser p(std::move(tokens)); + return p.parse(); +} + +template +inline EType* getTopExpr(const std::unique_ptr& p_stmnt) { + auto stmnt = dynamic_cast(p_stmnt.get()); + REQUIRE(stmnt != nullptr); + + auto expr = dynamic_cast(stmnt->m_expr.get()); + REQUIRE(expr != nullptr); + return expr; +} + +template +inline void checkStmntTypeAndValue(const std::unique_ptr& pStmnt, + const Expected& expected) { + ExprType* expr = nullptr; + auto stmnt = dynamic_cast(pStmnt.get()); + REQUIRE(stmnt != nullptr); + + if constexpr (std::same_as) + expr = dynamic_cast(stmnt->m_expr.get()); + else if constexpr (std::same_as) + expr = dynamic_cast(stmnt->m_initialiser.get()); + + if (!expr) FAIL("Could not properly downcast expression type."); + + if constexpr (std::same_as) { + REQUIRE(std::holds_alternative(expr->m_value)); + auto exprValue = std::get(expr->m_value); + REQUIRE(exprValue == expected); + } +} + +class AutoRestoreRdbuf { + std::ostream& out; + std::streambuf* old; + + public: + ~AutoRestoreRdbuf() { out.rdbuf(old); } + AutoRestoreRdbuf(const AutoRestoreRdbuf&) = delete; + AutoRestoreRdbuf(AutoRestoreRdbuf&&) = delete; + + AutoRestoreRdbuf(std::ostream& out) : out(out), old(out.rdbuf()) {} +}; + +template +inline std::string captureStream(std::ostream& out, Func&& fn, Args&&... args) { + AutoRestoreRdbuf restore{out}; + std::ostringstream oss; + out.rdbuf(oss.rdbuf()); + std::invoke(std::forward(fn), std::forward(args)...); + return oss.str(); +} + +inline Token makeToken(const std::string& name) { + return Token(TokenType::IDENTIFIER, name, std::monostate{}); +} +} // namespace helpers + +#endif diff --git a/test/cmd/test_interpreter.cpp b/test/cmd/test_interpreter.cpp new file mode 100644 index 0000000..98d6bda --- /dev/null +++ b/test/cmd/test_interpreter.cpp @@ -0,0 +1,353 @@ +#include +#include +#include +#include + +#include "interpreter.hpp" +#include "object.hpp" +#include "test_helpers.hpp" + +TEST_CASE("Test truthy behavior through bang operator", + "[interpreter][truthy]") { + Interpreter interp; + + SECTION("False and nil are falsy") { + auto stmnt1 = helpers::getStmnt("!!false"); + Object result1 = interp.interpret(stmnt1); + REQUIRE(result1 == Object{false}); + + auto stmnt2 = helpers::getStmnt("!!nil"); + Object result2 = interp.interpret(stmnt2); + REQUIRE(result2 == Object{false}); + } + + SECTION("Everything else is truthy") { + auto [input] = GENERATE(table({ + {"!!true"}, + {"!!0"}, + {"!!1"}, + {"!!-1"}, + {"!!\"\""}, + {"!!\"hello\""}, + })); + + auto stmnt = helpers::getStmnt(input); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{true}); + } + + SECTION("Single bang inverts truthiness") { + auto stmnt1 = helpers::getStmnt("!false"); + REQUIRE(interp.interpret(stmnt1) == Object{true}); + + auto stmnt2 = helpers::getStmnt("!true"); + REQUIRE(interp.interpret(stmnt2) == Object{false}); + + auto stmnt3 = helpers::getStmnt("!nil"); + REQUIRE(interp.interpret(stmnt3) == Object{true}); + + auto stmnt4 = helpers::getStmnt("!5"); + REQUIRE(interp.interpret(stmnt4) == Object{false}); + } +} + +TEST_CASE("Test stringify with different object types", + "[interpreter][stringify]") { + Interpreter interp; + + SECTION("Numbers") { + REQUIRE(interp.stringify(Object{42.0}) == "42"); + REQUIRE(interp.stringify(Object{3.14}) == "3.14"); + REQUIRE(interp.stringify(Object{0.0}) == "0"); + REQUIRE(interp.stringify(Object{-5.5}) == "-5.5"); + } + + SECTION("Booleans") { + REQUIRE(interp.stringify(Object{true}) == "true"); + REQUIRE(interp.stringify(Object{false}) == "false"); + } + + SECTION("Strings") { + REQUIRE(interp.stringify(Object{"hello"}) == "hello"); + REQUIRE(interp.stringify(Object{""}) == ""); + REQUIRE(interp.stringify(Object{"test string"}) == "test string"); + } + + SECTION("Nil/monostate") { + REQUIRE(interp.stringify(Object{std::monostate{}}) == "(null)"); + } +} + +TEST_CASE("Test literal expression evaluation", "[interpreter][evaluate]") { + Interpreter interp; + + auto [input, expected_value] = GENERATE(table({ + {"42", Object{42.0}}, + {"3.14", Object{3.14}}, + {"true", Object{true}}, + {"false", Object{false}}, + {"\"hello\"", Object{"hello"}}, + {"nil", Object{std::monostate{}}}, + })); + + auto stmnt = helpers::getStmnt(input); + Object result = interp.interpret(stmnt); + + REQUIRE(result == expected_value); +} + +TEST_CASE("Test grouping expression evaluation", "[interpreter][evaluate]") { + Interpreter interp; + + SECTION("Simple grouping") { + auto stmnt = helpers::getStmnt("(5)"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{5.0}); + } + + SECTION("Grouping with expression") { + auto stmnt = helpers::getStmnt("(2 + 3)"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{5.0}); + } + + SECTION("Nested grouping") { + auto stmnt = helpers::getStmnt("((5))"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{5.0}); + } +} + +TEST_CASE("Test unary expression evaluation", + "[interpreter][evaluate][unary]") { + Interpreter interp; + + SECTION("Unary minus on numbers") { + auto stmnt = helpers::getStmnt("-5"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{-5.0}); + + stmnt = helpers::getStmnt("-(-3)"); + result = interp.interpret(stmnt); + REQUIRE(result == Object{3.0}); + } + + SECTION("Unary bang on booleans") { + auto stmnt = helpers::getStmnt("!true"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{false}); + + stmnt = helpers::getStmnt("!false"); + result = interp.interpret(stmnt); + REQUIRE(result == Object{true}); + } + + SECTION("Unary bang on other types") { + auto stmnt = helpers::getStmnt("!5"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{false}); + + stmnt = helpers::getStmnt("!nil"); + result = interp.interpret(stmnt); + REQUIRE(result == Object{true}); + + stmnt = helpers::getStmnt("!\"hello\""); + result = interp.interpret(stmnt); + REQUIRE(result == Object{false}); + } +} + +TEST_CASE("Test binary arithmetic expression evaluation", + "[interpreter][evaluate][binary]") { + Interpreter interp; + + auto [input, expected] = GENERATE(table({ + {"2 + 3", 5.0}, + {"10 - 4", 6.0}, + {"3 * 4", 12.0}, + {"15 / 3", 5.0}, + {"2 + 3 * 4", 14.0}, + {"(2 + 3) * 4", 20.0}, + {"10 - 2 - 3", 5.0}, + })); + + auto stmnt = helpers::getStmnt(input); + Object result = interp.interpret(stmnt); + REQUIRE(std::get(result) == expected); +} + +TEST_CASE("Test binary comparison expression evaluation", + "[interpreter][evaluate][binary]") { + Interpreter interp; + + auto [input, expected] = GENERATE(table({ + {"5 > 3", true}, + {"3 > 5", false}, + {"5 >= 5", true}, + {"3 >= 5", false}, + {"3 < 5", true}, + {"5 < 3", false}, + {"5 <= 5", true}, + {"5 <= 3", false}, + })); + + auto stmnt = helpers::getStmnt(input); + Object result = interp.interpret(stmnt); + REQUIRE(std::get(result) == expected); +} + +TEST_CASE("Test binary equality expression evaluation", + "[interpreter][evaluate][binary]") { + Interpreter interp; + + auto [input, expected] = GENERATE(table({ + {"5 == 5", true}, + {"5 == 3", false}, + {"5 != 3", true}, + {"5 != 5", false}, + {"true == true", true}, + {"true == false", false}, + {"\"hello\" == \"hello\"", true}, + {"\"hello\" == \"world\"", false}, + {"nil == nil", true}, + {"5 == true", false}, + {"5 != true", true}, + })); + + auto stmnt = helpers::getStmnt(input); + Object result = interp.interpret(stmnt); + REQUIRE(std::get(result) == expected); +} + +TEST_CASE("Test visitVarStmnt - declaration with initializer", + "[interpreter][visitVarStmnt]") { + Interpreter interp; + auto stmnt = helpers::getStmnt("var testvar1 = 42"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{std::monostate{}}); +} + +TEST_CASE("Test visitVarStmnt - declaration without initializer", + "[interpreter][visitVarStmnt]") { + Interpreter interp; + auto stmnt = helpers::getStmnt("var testvar2"); + Object result = interp.interpret(stmnt); + REQUIRE(result == Object{std::monostate{}}); +} + +TEST_CASE("Test visitVariableExpr - retrieve number variable", + "[interpreter][visitVariableExpr]") { + Interpreter interp; + auto declStmnt = helpers::getStmnt("var numvar = 123"); + interp.interpret(declStmnt); + + auto varStmnt = helpers::getStmnt("numvar"); + Object result = interp.interpret(varStmnt); + REQUIRE(result == Object{123.0}); +} + +TEST_CASE("Test visitVariableExpr - retrieve string variable", + "[interpreter][visitVariableExpr]") { + Interpreter interp; + auto declStmnt = helpers::getStmnt("var stringvar = \"test\""); + interp.interpret(declStmnt); + + auto varStmnt = helpers::getStmnt("stringvar"); + Object result = interp.interpret(varStmnt); + REQUIRE(result == Object{"test"}); +} + +TEST_CASE("Test visitVariableExpr - retrieve boolean variable", + "[interpreter][visitVariableExpr]") { + Interpreter interp; + auto declStmnt = helpers::getStmnt("var boolvar = true"); + interp.interpret(declStmnt); + + auto varStmnt = helpers::getStmnt("boolvar"); + Object result = interp.interpret(varStmnt); + REQUIRE(result == Object{true}); +} + +TEST_CASE("Test visitAssignExpr - assignment returns value", + "[interpreter][visitAssignExpr]") { + Interpreter interp; + auto declStmnt = helpers::getStmnt("var assignvar = 10"); + interp.interpret(declStmnt); + + auto assignStmnt = helpers::getStmnt("assignvar = 20"); + Object result = interp.interpret(assignStmnt); + REQUIRE(result == Object{20.0}); +} + +TEST_CASE("Test visitAssignExpr - assignment with expression", + "[interpreter][visitAssignExpr]") { + Interpreter interp; + auto declStmnt = helpers::getStmnt("var exprvar = 5"); + interp.interpret(declStmnt); + + auto assignStmnt = helpers::getStmnt("exprvar = exprvar + 10"); + Object result = interp.interpret(assignStmnt); + REQUIRE(result == Object{15.0}); +} + +TEST_CASE("Test visitAssignExpr - assignment changes value", + "[interpreter][visitAssignExpr]") { + Interpreter interp; + auto declStmnt = helpers::getStmnt("var changevar = 1"); + interp.interpret(declStmnt); + + auto assignStmnt = helpers::getStmnt("changevar = 100"); + interp.interpret(assignStmnt); + + auto varStmnt = helpers::getStmnt("changevar"); + Object result = interp.interpret(varStmnt); + REQUIRE(result == Object{100.0}); +} + +TEST_CASE("Test visitCallStmnt - print function", + "[interpreter][visitCallStmnt]") { + Interpreter interp; + + auto stmnt = helpers::getStmnt("print 42"); + REQUIRE_NOTHROW(interp.interpret(stmnt)); +} + +TEST_CASE("Test visitCallStmnt - function argument mismatch errors", + "[interpreter][visitCallStmnt][error]") { + Interpreter interp; + + SECTION("Too many arguments to print") { + auto captured = helpers::captureStream(std::cerr, [&interp]() { + auto stmnt = helpers::getStmnt("print 42 43"); + interp.interpret(stmnt); + }); + REQUIRE( + captured.find("Function requires 1 arguments but 2 were provided") != + std::string::npos); + } +} + +TEST_CASE("Test variable in arithmetic expression", + "[interpreter][integration]") { + Interpreter interp; + auto declStmnt = helpers::getStmnt("var mathvartest = 5"); + interp.interpret(declStmnt); + + auto exprStmnt = helpers::getStmnt("mathvartest * 2 + 3"); + Object result = interp.interpret(exprStmnt); + REQUIRE(result == Object{13.0}); +} + +TEST_CASE("Test multiple variables in expression", + "[interpreter][integration]") { + Interpreter interp; + auto decl1 = helpers::getStmnt("var avartest = 10"); + interp.interpret(decl1); + + auto decl2 = helpers::getStmnt("var bvartest = 5"); + interp.interpret(decl2); + + auto exprStmnt = helpers::getStmnt("avartest - bvartest"); + Object result = interp.interpret(exprStmnt); + REQUIRE(result == Object{5.0}); +} diff --git a/test/cmd/test_object.cpp b/test/cmd/test_object.cpp new file mode 100644 index 0000000..8503bc3 --- /dev/null +++ b/test/cmd/test_object.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include + +#include "object.hpp" +#include "test_helpers.hpp" + +// This whole file is ugly but it works and I can't find it a better way to do +// it + +TEST_CASE("Test arithmetic operators between all type combinations", + "[object][operator][arithmetic]") { + auto [op_name, op, lhs, rhs, should_succeed, expected_type] = GENERATE( + table, + Object, Object, bool, std::string>({ + // Addition + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{5.0}, Object{3.0}, true, "double"}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{5.0}, Object{true}, true, "double"}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{true}, Object{3.0}, true, "double"}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{true}, Object{false}, true, "double"}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{"hello"}, Object{" world"}, true, "string"}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{5.0}, Object{"hello"}, false, ""}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{"hello"}, Object{5.0}, false, ""}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{true}, Object{"hello"}, false, ""}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{"hello"}, Object{true}, false, ""}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{std::monostate{}}, Object{5.0}, false, ""}, + {"+", [](const Object& l, const Object& r) { return l + r; }, + Object{5.0}, Object{std::monostate{}}, false, ""}, + + // Subtraction + {"-", [](const Object& l, const Object& r) { return l - r; }, + Object{5.0}, Object{3.0}, true, "double"}, + {"-", [](const Object& l, const Object& r) { return l - r; }, + Object{5.0}, Object{true}, true, "double"}, + {"-", [](const Object& l, const Object& r) { return l - r; }, + Object{true}, Object{false}, true, "double"}, + {"-", [](const Object& l, const Object& r) { return l - r; }, + Object{"hello"}, Object{"world"}, false, ""}, + {"-", [](const Object& l, const Object& r) { return l - r; }, + Object{5.0}, Object{"hello"}, false, ""}, + {"-", [](const Object& l, const Object& r) { return l - r; }, + Object{std::monostate{}}, Object{5.0}, false, ""}, + + // Multiplication + {"*", [](const Object& l, const Object& r) { return l * r; }, + Object{5.0}, Object{3.0}, true, "double"}, + {"*", [](const Object& l, const Object& r) { return l * r; }, + Object{5.0}, Object{true}, true, "double"}, + {"*", [](const Object& l, const Object& r) { return l * r; }, + Object{true}, Object{false}, true, "double"}, + {"*", [](const Object& l, const Object& r) { return l * r; }, + Object{"hello"}, Object{"world"}, false, ""}, + {"*", [](const Object& l, const Object& r) { return l * r; }, + Object{5.0}, Object{"hello"}, false, ""}, + {"*", [](const Object& l, const Object& r) { return l * r; }, + Object{std::monostate{}}, Object{5.0}, false, ""}, + + // Division + {"/", [](const Object& l, const Object& r) { return l / r; }, + Object{6.0}, Object{3.0}, true, "double"}, + {"/", [](const Object& l, const Object& r) { return l / r; }, + Object{5.0}, Object{true}, true, "double"}, + {"/", [](const Object& l, const Object& r) { return l / r; }, + Object{true}, Object{true}, true, "double"}, + {"/", [](const Object& l, const Object& r) { return l / r; }, + Object{"hello"}, Object{"world"}, false, ""}, + {"/", [](const Object& l, const Object& r) { return l / r; }, + Object{5.0}, Object{"hello"}, false, ""}, + {"/", [](const Object& l, const Object& r) { return l / r; }, + Object{std::monostate{}}, Object{5.0}, false, ""}, + })); + + if (should_succeed) { + Object res = op(lhs, rhs); + if (expected_type == "double") { + REQUIRE(std::holds_alternative(res)); + } else if (expected_type == "string") { + REQUIRE(std::holds_alternative(res)); + } + } else { + auto captured = helpers::captureStream(std::cerr, op, lhs, rhs); + bool has_string_error = + captured.find(std::format("Cannot apply operator {} to strings", + op_name)) != std::string::npos; + bool has_unsupported_error = + captured.find("Unsupported operand types") != std::string::npos; + REQUIRE((has_string_error || has_unsupported_error)); + } +} + +TEST_CASE("Test comparison operators between all type combinations", + "[object][operator][comparison]") { + auto [op_name, op, lhs, rhs, should_succeed] = GENERATE( + table, + Object, Object, bool>({ + // Less than + {"<", [](const Object& l, const Object& r) { return l < r; }, + Object{5.0}, Object{3.0}, true}, + {"<", [](const Object& l, const Object& r) { return l < r; }, + Object{5.0}, Object{true}, true}, + {"<", [](const Object& l, const Object& r) { return l < r; }, + Object{true}, Object{false}, true}, + {"<", [](const Object& l, const Object& r) { return l < r; }, + Object{"hello"}, Object{"world"}, false}, + {"<", [](const Object& l, const Object& r) { return l < r; }, + Object{5.0}, Object{"hello"}, false}, + {"<", [](const Object& l, const Object& r) { return l < r; }, + Object{std::monostate{}}, Object{5.0}, false}, + + // Greater than + {">", [](const Object& l, const Object& r) { return l > r; }, + Object{5.0}, Object{3.0}, true}, + {">", [](const Object& l, const Object& r) { return l > r; }, + Object{5.0}, Object{true}, true}, + {">", [](const Object& l, const Object& r) { return l > r; }, + Object{true}, Object{false}, true}, + {">", [](const Object& l, const Object& r) { return l > r; }, + Object{"hello"}, Object{"world"}, false}, + {">", [](const Object& l, const Object& r) { return l > r; }, + Object{5.0}, Object{"hello"}, false}, + {">", [](const Object& l, const Object& r) { return l > r; }, + Object{std::monostate{}}, Object{5.0}, false}, + + // Less than or equal to + {"<=", [](const Object& l, const Object& r) { return l <= r; }, + Object{5.0}, Object{3.0}, true}, + {"<=", [](const Object& l, const Object& r) { return l <= r; }, + Object{5.0}, Object{true}, true}, + {"<=", [](const Object& l, const Object& r) { return l <= r; }, + Object{true}, Object{false}, true}, + {"<=", [](const Object& l, const Object& r) { return l <= r; }, + Object{"hello"}, Object{"world"}, false}, + {"<=", [](const Object& l, const Object& r) { return l <= r; }, + Object{5.0}, Object{"hello"}, false}, + {"<=", [](const Object& l, const Object& r) { return l <= r; }, + Object{std::monostate{}}, Object{5.0}, false}, + + // Greater than or equal to + {">=", [](const Object& l, const Object& r) { return l >= r; }, + Object{5.0}, Object{3.0}, true}, + {">=", [](const Object& l, const Object& r) { return l >= r; }, + Object{5.0}, Object{true}, true}, + {">=", [](const Object& l, const Object& r) { return l >= r; }, + Object{true}, Object{false}, true}, + {">=", [](const Object& l, const Object& r) { return l >= r; }, + Object{"hello"}, Object{"world"}, false}, + {">=", [](const Object& l, const Object& r) { return l >= r; }, + Object{5.0}, Object{"hello"}, false}, + {">=", [](const Object& l, const Object& r) { return l >= r; }, + Object{std::monostate{}}, Object{5.0}, false}, + })); + + if (should_succeed) { + bool result = op(lhs, rhs); + REQUIRE((result == true || result == false)); + } else { + auto captured = helpers::captureStream(std::cerr, op, lhs, rhs); + REQUIRE(captured.find(std::format( + "Cannot apply operator {} to non-arithmetic types", op_name)) != + std::string::npos); + } +} + +TEST_CASE("Test equality operators between all type combinations", + "[object][operator][equality]") { + auto [op_name, op, lhs, rhs, expected_result] = GENERATE( + table, + Object, Object, bool>({ + // == operator - same types + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{5.0}, Object{5.0}, true}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{true}, Object{true}, true}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{"hello"}, Object{"hello"}, true}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{std::monostate{}}, Object{std::monostate{}}, true}, + + // == operator - different values, same types + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{5.0}, Object{3.0}, false}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{true}, Object{false}, false}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{"hello"}, Object{"world"}, false}, + + // == operator - different types + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{5.0}, Object{true}, false}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{5.0}, Object{"hello"}, false}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{true}, Object{"hello"}, false}, + {"==", [](const Object& l, const Object& r) { return l == r; }, + Object{std::monostate{}}, Object{5.0}, false}, + + // != operator - same types + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{5.0}, Object{5.0}, false}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{true}, Object{true}, false}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{"hello"}, Object{"hello"}, false}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{std::monostate{}}, Object{std::monostate{}}, false}, + + // != operator - different values, same types + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{5.0}, Object{3.0}, true}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{true}, Object{false}, true}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{"hello"}, Object{"world"}, true}, + + // != operator - different types + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{5.0}, Object{true}, true}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{5.0}, Object{"hello"}, true}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{true}, Object{"hello"}, true}, + {"!=", [](const Object& l, const Object& r) { return l != r; }, + Object{std::monostate{}}, Object{5.0}, true}, + })); + + bool result = op(lhs, rhs); + REQUIRE(result == expected_result); +} diff --git a/test/cmd/test_parser.cpp b/test/cmd/test_parser.cpp new file mode 100644 index 0000000..e18d601 --- /dev/null +++ b/test/cmd/test_parser.cpp @@ -0,0 +1,262 @@ +#include +#include +#include +#include +#include + +#include "expr.hpp" +#include "object.hpp" +#include "stmnt.hpp" +#include "test_helpers.hpp" +#include "token.hpp" + +TEST_CASE("Test literal parsing", "[parser][expression][literal]") { + auto [input, expected_value] = + GENERATE(table({{"42", 42.0}, + {"3.14", 3.14}, + {"0", 0.0}, + {"\"hello\"", "hello"}, + {"false", false}, + {"true", true}, + {"nil", std::monostate{}}})); + + auto stmnt = helpers::getStmnt(input); + auto visitor = [&stmnt](const auto& obj) -> void { + helpers::checkStmntTypeAndValue(stmnt, obj); + }; + + std::visit(visitor, expected_value); + + SECTION("Function parsing") { + auto stmnt = helpers::getStmnt("print 5"); + auto callStmnt = dynamic_cast(stmnt.get()); + REQUIRE(callStmnt != nullptr); + REQUIRE(callStmnt->m_fn.m_type == TokenType::IDENTIFIER); + } +} + +TEST_CASE("Test variable parsing", "[parser][variable][assign]") { + auto [input, expected_value] = GENERATE(table({ + {"var x = 5", 5.0}, + {"var x = \"hello\"", "hello"}, + {"var x = true", true}, + {"var x = nil", std::monostate{}}, + })); + + auto stmnt = helpers::getStmnt(input); + + auto visitor = [&stmnt](const auto& obj) -> void { + helpers::checkStmntTypeAndValue(stmnt, obj); + }; + + std::visit(visitor, expected_value); + + SECTION("Test variable parsing with functions as values") { + auto stmnt = helpers::getStmnt("var x = print"); + auto varStmnt = dynamic_cast(stmnt.get()); + REQUIRE(varStmnt != nullptr); + auto var = dynamic_cast(varStmnt->m_initialiser.get()); + REQUIRE(var != nullptr); + } + + SECTION("Variable re-assignment") { + auto stmnt = helpers::getStmnt("x = 5"); + auto assign = helpers::getTopExpr(stmnt); + REQUIRE(assign->m_name.m_lexeme == "x"); + } +} + +TEST_CASE("Test grouping expression parsing", + "[parser][expression][grouping]") { + SECTION("Simple grouping") { + auto stmnt = helpers::getStmnt("(5)"); + REQUIRE(helpers::getTopExpr(stmnt) != nullptr); + } + + SECTION("Grouping with operator") { + auto stmnt = helpers::getStmnt("(5+3)"); + auto grouping = helpers::getTopExpr(stmnt); + auto binary = dynamic_cast(grouping->m_expr.get()); + + REQUIRE(binary != nullptr); + REQUIRE(binary->m_op.m_type == TokenType::PLUS); + } + + SECTION("Grouping overrides precedence") { + auto stmnt = helpers::getStmnt("(2+3)*4"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::STAR); + auto grouping = dynamic_cast(binary->m_left.get()); + REQUIRE(grouping != nullptr); + } + + SECTION("Grouping on the right side") { + auto stmnt = helpers::getStmnt("2*(3+4)"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::STAR); + auto grouping = dynamic_cast(binary->m_right.get()); + REQUIRE(grouping != nullptr); + } + + SECTION("Grouping with unary operator") { + auto stmnt = helpers::getStmnt("-(3+4)"); + auto unary = helpers::getTopExpr(stmnt); + REQUIRE(unary->m_op.m_type == TokenType::MINUS); + auto grouping = dynamic_cast(unary->m_right.get()); + REQUIRE(grouping != nullptr); + } + + SECTION("String grouping expression") { + auto stmnt = helpers::getStmnt("(\"hello\"+\"world\")"); + auto grouping = helpers::getTopExpr(stmnt); + auto binary = dynamic_cast(grouping->m_expr.get()); + REQUIRE(binary != nullptr); + REQUIRE(binary->m_op.m_type == TokenType::PLUS); + } +} + +TEST_CASE("Test unary expression parsing", "[parser][expression][unary]") { + SECTION("Unary expression on number") { + auto stmnt = helpers::getStmnt("-5"); + auto unary = helpers::getTopExpr(stmnt); + REQUIRE(unary->m_op.m_type == TokenType::MINUS); + } + + SECTION("Unary expression on boolean") { + auto stmnt = helpers::getStmnt("!true"); + auto unary = helpers::getTopExpr(stmnt); + REQUIRE(unary->m_op.m_type == TokenType::BANG); + } +} + +TEST_CASE("Test arithmetic operator parsing", + "[parser][expression][operator][binary]") { + auto [op, tokenType] = + GENERATE(table({{"+", TokenType::PLUS}, + {"-", TokenType::MINUS}, + {"*", TokenType::STAR}, + {"/", TokenType::SLASH}})); + + auto stmnt = helpers::getStmnt(std::format("3{}5", op)); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == tokenType); +} + +TEST_CASE("Test arithmetic operator precedence and associativity", + "[parser][expression][operator][binary]") { + SECTION("Multiplication before addition") { + auto stmnt = helpers::getStmnt("2+3*4"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::PLUS); + + auto rhs = dynamic_cast(binary->m_right.get()); + REQUIRE(rhs != nullptr); + REQUIRE(rhs->m_op.m_type == TokenType::STAR); + } + + SECTION("Division before subtraction") { + auto stmnt = helpers::getStmnt("3-4/2"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::MINUS); + + auto rhs = dynamic_cast(binary->m_right.get()); + REQUIRE(rhs != nullptr); + REQUIRE(rhs->m_op.m_type == TokenType::SLASH); + } + + SECTION("Associativity") { + auto stmnt = helpers::getStmnt("5+3-2"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::MINUS); + + auto lhs = dynamic_cast(binary->m_left.get()); + REQUIRE(lhs != nullptr); + REQUIRE(lhs->m_op.m_type == TokenType::PLUS); + } +} + +TEST_CASE("Test comparison operators", "[parser][expressions][operators]") { + SECTION("Basic operator parsing") { + auto [op, tokenType] = GENERATE( + table({{"<", TokenType::LESS}, + {">", TokenType::GREATER}, + {"<=", TokenType::LESS_EQUAL}, + {">=", TokenType::GREATER_EQUAL}})); + + auto stmnt = helpers::getStmnt(std::format("3{}5", op)); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == tokenType); + } + + SECTION("Arithmetic before comparison") { + auto stmnt = helpers::getStmnt("2 + 3 < 4 * 2"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::LESS); + + auto lhs = dynamic_cast(binary->m_left.get()); + REQUIRE(lhs != nullptr); + REQUIRE(lhs->m_op.m_type == TokenType::PLUS); + + auto rhs = dynamic_cast(binary->m_right.get()); + REQUIRE(rhs != nullptr); + REQUIRE(rhs->m_op.m_type == TokenType::STAR); + } + + SECTION("Subtraction before comparison") { + auto stmnt = helpers::getStmnt("10 - 2 > 5"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::GREATER); + + auto lhs = dynamic_cast(binary->m_left.get()); + REQUIRE(lhs != nullptr); + REQUIRE(lhs->m_op.m_type == TokenType::MINUS); + } + + SECTION("Comparison before equality") { + auto stmnt = helpers::getStmnt("3+5 == 5+3"); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == TokenType::EQUAL_EQUAL); + + auto lhs = dynamic_cast(binary->m_left.get()); + REQUIRE(lhs != nullptr); + REQUIRE(lhs->m_op.m_type == TokenType::PLUS); + + auto rhs = dynamic_cast(binary->m_right.get()); + REQUIRE(rhs != nullptr); + REQUIRE(rhs->m_op.m_type == TokenType::PLUS); + } +} + +TEST_CASE("Test equality operator parsing", + "[parser][expressions][operators]") { + auto [op, tokenType] = GENERATE(table( + {{"==", TokenType::EQUAL_EQUAL}, {"!=", TokenType::BANG_EQUAL}})); + + auto stmnt = helpers::getStmnt(std::format("3{}5", op)); + auto binary = helpers::getTopExpr(stmnt); + REQUIRE(binary->m_op.m_type == tokenType); +} + +TEST_CASE("Test parser error handling", "[parser][expressions][errors]") { + SECTION("Invalid primary expression") { + auto captured = helpers::captureStream(std::cerr, helpers::getStmnt, ")"); + REQUIRE(captured.find("Expected expression") != std::string::npos); + } + + SECTION("Missing closing parenthesis") { + auto captured = helpers::captureStream(std::cerr, helpers::getStmnt, "(5"); + REQUIRE(captured.find("Expect \')\' after expression") != + std::string::npos); + } + + SECTION("Variable declaration without name") { + auto captured = helpers::captureStream(std::cerr, helpers::getStmnt, "var"); + REQUIRE(captured.find("Expect variable name") != std::string::npos); + } + + SECTION("Invalid assignment target") { + auto captured = + helpers::captureStream(std::cerr, helpers::getStmnt, "5=10"); + REQUIRE(captured.find("Invalid assignment target") != std::string::npos); + } +} diff --git a/test/cmd/test_scanner.cpp b/test/cmd/test_scanner.cpp new file mode 100644 index 0000000..b2a4b0b --- /dev/null +++ b/test/cmd/test_scanner.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include "test_helpers.hpp" +#include "token.hpp" + +TEST_CASE("Test single-character tokenisation", "[scanner]") { + auto [input, expected_type] = + GENERATE(table({{"(", TokenType::LEFT_PAREN}, + {")", TokenType::RIGHT_PAREN}, + {"-", TokenType::MINUS}, + {"+", TokenType::PLUS}, + {"*", TokenType::STAR}, + {"!", TokenType::BANG}, + {"=", TokenType::EQUAL}, + {"<", TokenType::LESS}, + {">", TokenType::GREATER}, + {"/", TokenType::SLASH}})); + + auto tokens = helpers::scan(input); + REQUIRE(helpers::checkTokensSize(tokens.size(), 1)); + REQUIRE(tokens[0].m_type == expected_type); +} + +TEST_CASE("Test two-character tokenisation", "[scanner]") { + auto [input, expected_type] = GENERATE( + table({{"!=", TokenType::BANG_EQUAL}, + {"==", TokenType::EQUAL_EQUAL}, + {"<=", TokenType::LESS_EQUAL}, + {">=", TokenType::GREATER_EQUAL}})); + + auto tokens = helpers::scan(input); + REQUIRE(helpers::checkTokensSize(tokens.size(), 1)); + REQUIRE(tokens[0].m_type == expected_type); +} + +TEST_CASE("Test single-char vs two-char operators", "[scanner]") { + auto [input, first_type, second_type] = + GENERATE(table( + {{"! =", TokenType::BANG, TokenType::EQUAL}, + {"= =", TokenType::EQUAL, TokenType::EQUAL}, + {"< =", TokenType::LESS, TokenType::EQUAL}, + {"> =", TokenType::GREATER, TokenType::EQUAL}})); + + auto tokens = helpers::scan(input); + REQUIRE(helpers::checkTokensSize(tokens.size(), 2)); + REQUIRE(tokens[0].m_type == first_type); + REQUIRE(tokens[1].m_type == second_type); +} + +TEST_CASE("Test string and number tokenisation", "[scanner]") { + auto [input, expected_type] = GENERATE( + table({{"\"hello\"", TokenType::STRING}, + {"5", TokenType::NUMBER}, + {"5.10", TokenType::NUMBER}, + {"55", TokenType::NUMBER}, + {"varName", TokenType::IDENTIFIER}})); + + auto tokens = helpers::scan(input); + REQUIRE(helpers::checkTokensSize(tokens.size(), 1)); + REQUIRE(tokens[0].m_type == expected_type); +} + +TEST_CASE("Test errors on malformed input", "[scanner]") { + auto [input, error_message] = GENERATE(table( + {{"12.", "Error : Unexpected character"}, + {"\"string", "Error : Unterminated string"}})); + + std::string captured{}; + try { + captured = helpers::captureStream(std::cerr, helpers::scan, input); + } catch (...) { + REQUIRE(captured == error_message); + } +} diff --git a/vcpkg b/vcpkg index e3ed418..b1e15ef 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit e3ed41868d5034bc608eaaa58383cd6ecdbb5ffb +Subproject commit b1e15efef6758eaa0beb0a8732cfa66f6a68a81d diff --git a/vcpkg.json b/vcpkg.json index ce26704..c23a1c5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,6 +2,7 @@ "name": "caesar", "version": "0.1.0", "dependencies": [ - "libdwarf" + "libdwarf", + "catch2" ] }