diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000..7fd10f6 --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,40 @@ +name: CMake + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + strategy: + matrix: + cmake_build_type: [Asan, Release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Build Essential + run: sudo apt update && sudo apt install build-essential + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + create-symlink: true + key: ${{ github.job }}-${{ matrix.os }} + - name: Configure + working-directory: ${{github.workspace}}/ports/cpp + run: | + mkdir build + cd build + cmake \ + -DANTLR4C3_DEVELOPER=ON \ + -DCMAKE_BUILD_TYPE=${{ matrix.cmake_build_type }} \ + .. + - name: Build + working-directory: ${{github.workspace}}/ports/cpp/build + run: make + - name: Unit Test + working-directory: ${{github.workspace}}/ports/cpp/build/test + run: | + ctest + cat Testing/Temporary/LastTest.log diff --git a/ports/cpp/.clangd b/ports/cpp/.clangd new file mode 100644 index 0000000..141c598 --- /dev/null +++ b/ports/cpp/.clangd @@ -0,0 +1,4 @@ +InlayHints: + Designators: No + Enabled: No + DeducedTypes: No diff --git a/ports/cpp/.gitignore b/ports/cpp/.gitignore new file mode 100644 index 0000000..ca3dca4 --- /dev/null +++ b/ports/cpp/.gitignore @@ -0,0 +1,6 @@ +# CMake +build/ +build-asan/ + +# Clangd +.cache diff --git a/ports/cpp/.vscode/launch.json b/ports/cpp/.vscode/launch.json new file mode 100644 index 0000000..afd6056 --- /dev/null +++ b/ports/cpp/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug Test", + "program": "${workspaceFolder}/build/test/${fileDirnameBasename}/antlr4-c3-test", + "args": [], + "cwd": "${workspaceFolder}", + "preLaunchTask": "Build All" + } + ] +} \ No newline at end of file diff --git a/ports/cpp/.vscode/settings.json b/ports/cpp/.vscode/settings.json new file mode 100644 index 0000000..fff06c8 --- /dev/null +++ b/ports/cpp/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "clangd.arguments": [ + "-compile-commands-dir=./build", + "-header-insertion=never" + ] +} \ No newline at end of file diff --git a/ports/cpp/.vscode/tasks.json b/ports/cpp/.vscode/tasks.json new file mode 100644 index 0000000..7438cc5 --- /dev/null +++ b/ports/cpp/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build All", + "type": "shell", + "command": [ + "make", + "all" + ], + "options": { + "cwd": "${workspaceFolder}/build" + } + } + ] +} \ No newline at end of file diff --git a/ports/cpp/CMakeLists.txt b/ports/cpp/CMakeLists.txt new file mode 100644 index 0000000..35466b9 --- /dev/null +++ b/ports/cpp/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.7) +project(antlr4-c3 VERSION 1.1.0) + +option(ANTLR4C3_DEVELOPER "Enable ${PROJECT_NAME} developer mode" OFF) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") + +include(cmake/CompileOptions.cmake) +include(cmake/Sanitize.cmake) + +set(ANTLR4_TAG 4.13.1) + +find_package(Antlr4Cpp REQUIRED) + +add_subdirectory(source) + +if (ANTLR4C3_DEVELOPER) + include(cmake/Testing.cmake) + + find_package(Antlr4Tool REQUIRED) + find_package(GTest REQUIRED) + + add_subdirectory(test) +endif() diff --git a/ports/cpp/README.md b/ports/cpp/README.md new file mode 100644 index 0000000..83e1d18 --- /dev/null +++ b/ports/cpp/README.md @@ -0,0 +1,59 @@ +# ANTLRv4 C3 C++ Port + +## About + +This is a port of the `antlr4-c3` library to `C++`. + +Please see the parent [README.md](../../readme.md) for an explanation of the library and for examples. + +## Port features + +1. Only `CodeCompletionCore` was ported. + +2. Supports cancellation for `collectCandidates` method via timeout or flag. + +## Requirements + +- [C++ 23 standard](https://en.cppreference.com/w/cpp/23) to compile sources. + +- [ANTLRv4 C++ Runtime](https://github.com/antlr/antlr4/tree/4.13.1/runtime/Cpp) to compile sources. + +- [CMake 3.7](https://cmake.org/cmake/help/latest/release/3.7.html) to build project. + +- [ANTLRv4 Tool](https://www.antlr.org/download.html) to build tests. + +- [Google Test](https://github.com/google/googletest) to build tests. + +## Usage + +Currently, there are no other ways to adding C++ port as a dependency other than by copying and pasting the [directory with project's source code](./source/antlr4-c3) into your own project. + +## Build & Run + +Actual build steps are available at [CMake GitHub Workflow](../../.github/workflows/cmake.yml). + +`ANTLRv4` Runtime and Tool as well as other dependecnies will be downloaded during `CMake` configiration stage. + +```bash +# Clone antlr4-c3 repository and enter C++ port directory +git clone git@github.com:mike-lischke/antlr4-c3.git +cd antlr4-c3/ports/cpp # Also a workspace directory for VSCode + +# Create and enter the build directory +mkdir build && cd build + +# Configure CMake build +# - ANTLR4C3_DEVELOPER should be enabled if you are going to run tests +# - CMAKE_BUILD_TYPE Asan and Tsan are supported too +cmake -DANTLR4C3_DEVELOPER=ON -DCMAKE_BUILD_TYPE=Release .. + +# Build everything +make + +# Running tests being at build directory +(make && cd test && ctest) +``` + +## Contributing + +We recommend using [VSCode](https://code.visualstudio.com/) with [clangd extension](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd) as an IDE. There are some configuration files for launching tests in debug mode, `clangd` configuration and more. diff --git a/ports/cpp/cmake/CompileOptions.cmake b/ports/cpp/cmake/CompileOptions.cmake new file mode 100644 index 0000000..f3c0c44 --- /dev/null +++ b/ports/cpp/cmake/CompileOptions.cmake @@ -0,0 +1,10 @@ +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_compile_options(-Wall -Wextra -Wpedantic) + +if(ANTLR4C3_DEVELOPER) + add_compile_options(-Werror) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +endif() diff --git a/ports/cpp/cmake/ExternalAntlr4Cpp.cmake b/ports/cpp/cmake/ExternalAntlr4Cpp.cmake new file mode 100644 index 0000000..3d1303d --- /dev/null +++ b/ports/cpp/cmake/ExternalAntlr4Cpp.cmake @@ -0,0 +1,185 @@ +# Source: https://github.com/antlr/antlr4/blob/4.13.1/runtime/Cpp/cmake/ExternalAntlr4Cpp.cmake + +cmake_minimum_required(VERSION 3.7) + +# Added additionally +if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP + cmake_policy(SET CMP0135 NEW) +endif() + +if(POLICY CMP0114) + cmake_policy(SET CMP0114 NEW) +endif() + +include(ExternalProject) + +set(ANTLR4_ROOT ${CMAKE_CURRENT_BINARY_DIR}/antlr4_runtime/src/antlr4_runtime) +set(ANTLR4_INCLUDE_DIRS ${ANTLR4_ROOT}/runtime/Cpp/runtime/src) +set(ANTLR4_GIT_REPOSITORY https://github.com/antlr/antlr4.git) +if(NOT DEFINED ANTLR4_TAG) + # Set to branch name to keep library updated at the cost of needing to rebuild after 'clean' + # Set to commit hash to keep the build stable and does not need to rebuild after 'clean' + set(ANTLR4_TAG master) +endif() + +# Ensure that the include dir already exists at configure time (to avoid cmake erroring +# on non-existent include dirs) +file(MAKE_DIRECTORY "${ANTLR4_INCLUDE_DIRS}") + +if(${CMAKE_GENERATOR} MATCHES "Visual Studio.*") + set(ANTLR4_OUTPUT_DIR ${ANTLR4_ROOT}/runtime/Cpp/runtime/$(Configuration)) +elseif(${CMAKE_GENERATOR} MATCHES "Xcode.*") + set(ANTLR4_OUTPUT_DIR ${ANTLR4_ROOT}/runtime/Cpp/runtime/$(CONFIGURATION)) +else() + set(ANTLR4_OUTPUT_DIR ${ANTLR4_ROOT}/runtime/Cpp/runtime) +endif() + +if(MSVC) + set(ANTLR4_STATIC_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/antlr4-runtime-static.lib) + set(ANTLR4_SHARED_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/antlr4-runtime.lib) + set(ANTLR4_RUNTIME_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/antlr4-runtime.dll) +else() + set(ANTLR4_STATIC_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.a) + if(MINGW) + set(ANTLR4_SHARED_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dll.a) + set(ANTLR4_RUNTIME_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dll) + elseif(CYGWIN) + set(ANTLR4_SHARED_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dll.a) + set(ANTLR4_RUNTIME_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/cygantlr4-runtime-4.13.1.dll) + elseif(APPLE) + set(ANTLR4_RUNTIME_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dylib) + else() + set(ANTLR4_RUNTIME_LIBRARIES + ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.so) + endif() +endif() + +if(${CMAKE_GENERATOR} MATCHES ".* Makefiles") + # This avoids + # 'warning: jobserver unavailable: using -j1. Add '+' to parent make rule.' + set(ANTLR4_BUILD_COMMAND $(MAKE)) +elseif(${CMAKE_GENERATOR} MATCHES "Visual Studio.*") + set(ANTLR4_BUILD_COMMAND + ${CMAKE_COMMAND} + --build . + --config $(Configuration) + --target) +elseif(${CMAKE_GENERATOR} MATCHES "Xcode.*") + set(ANTLR4_BUILD_COMMAND + ${CMAKE_COMMAND} + --build . + --config $(CONFIGURATION) + --target) +else() + set(ANTLR4_BUILD_COMMAND + ${CMAKE_COMMAND} + --build . + --target) +endif() + +if(NOT DEFINED ANTLR4_WITH_STATIC_CRT) + set(ANTLR4_WITH_STATIC_CRT ON) +endif() + +if(ANTLR4_ZIP_REPOSITORY) + ExternalProject_Add( + antlr4_runtime + PREFIX antlr4_runtime + URL ${ANTLR4_ZIP_REPOSITORY} + DOWNLOAD_DIR ${CMAKE_CURRENT_BINARY_DIR} + BUILD_COMMAND "" + BUILD_IN_SOURCE 1 + SOURCE_DIR ${ANTLR4_ROOT} + SOURCE_SUBDIR runtime/Cpp + CMAKE_CACHE_ARGS + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + -DWITH_STATIC_CRT:BOOL=${ANTLR4_WITH_STATIC_CRT} + -DDISABLE_WARNINGS:BOOL=ON + # -DCMAKE_CXX_STANDARD:STRING=17 # if desired, compile the runtime with a different C++ standard + # -DCMAKE_CXX_STANDARD:STRING=${CMAKE_CXX_STANDARD} # alternatively, compile the runtime with the same C++ standard as the outer project + INSTALL_COMMAND "" + EXCLUDE_FROM_ALL 1) +else() + ExternalProject_Add( + antlr4_runtime + PREFIX antlr4_runtime + GIT_REPOSITORY ${ANTLR4_GIT_REPOSITORY} + GIT_TAG ${ANTLR4_TAG} + DOWNLOAD_DIR ${CMAKE_CURRENT_BINARY_DIR} + BUILD_COMMAND "" + BUILD_IN_SOURCE 1 + SOURCE_DIR ${ANTLR4_ROOT} + SOURCE_SUBDIR runtime/Cpp + CMAKE_CACHE_ARGS + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + -DWITH_STATIC_CRT:BOOL=${ANTLR4_WITH_STATIC_CRT} + -DDISABLE_WARNINGS:BOOL=ON + # -DCMAKE_CXX_STANDARD:STRING=17 # if desired, compile the runtime with a different C++ standard + # -DCMAKE_CXX_STANDARD:STRING=${CMAKE_CXX_STANDARD} # alternatively, compile the runtime with the same C++ standard as the outer project + INSTALL_COMMAND "" + EXCLUDE_FROM_ALL 1) +endif() + +# Separate build step as rarely people want both +set(ANTLR4_BUILD_DIR ${ANTLR4_ROOT}) +if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.14.0") + # CMake 3.14 builds in above's SOURCE_SUBDIR when BUILD_IN_SOURCE is true + set(ANTLR4_BUILD_DIR ${ANTLR4_ROOT}/runtime/Cpp) +endif() + +ExternalProject_Add_Step( + antlr4_runtime + build_static + COMMAND ${ANTLR4_BUILD_COMMAND} antlr4_static + # Depend on target instead of step (a custom command) + # to avoid running dependent steps concurrently + DEPENDS antlr4_runtime + BYPRODUCTS ${ANTLR4_STATIC_LIBRARIES} + EXCLUDE_FROM_MAIN 1 + WORKING_DIRECTORY ${ANTLR4_BUILD_DIR}) +ExternalProject_Add_StepTargets(antlr4_runtime build_static) + +add_library(antlr4_static STATIC IMPORTED) +add_dependencies(antlr4_static antlr4_runtime-build_static) +set_target_properties(antlr4_static PROPERTIES + IMPORTED_LOCATION ${ANTLR4_STATIC_LIBRARIES}) +target_include_directories(antlr4_static + INTERFACE + ${ANTLR4_INCLUDE_DIRS} +) + +ExternalProject_Add_Step( + antlr4_runtime + build_shared + COMMAND ${ANTLR4_BUILD_COMMAND} antlr4_shared + # Depend on target instead of step (a custom command) + # to avoid running dependent steps concurrently + DEPENDS antlr4_runtime + BYPRODUCTS ${ANTLR4_SHARED_LIBRARIES} ${ANTLR4_RUNTIME_LIBRARIES} + EXCLUDE_FROM_MAIN 1 + WORKING_DIRECTORY ${ANTLR4_BUILD_DIR}) +ExternalProject_Add_StepTargets(antlr4_runtime build_shared) + +add_library(antlr4_shared SHARED IMPORTED) +add_dependencies(antlr4_shared antlr4_runtime-build_shared) +set_target_properties(antlr4_shared PROPERTIES + IMPORTED_LOCATION ${ANTLR4_RUNTIME_LIBRARIES}) +target_include_directories(antlr4_shared + INTERFACE + ${ANTLR4_INCLUDE_DIRS} +) + +if(ANTLR4_SHARED_LIBRARIES) + set_target_properties(antlr4_shared PROPERTIES + IMPORTED_IMPLIB ${ANTLR4_SHARED_LIBRARIES}) +endif() diff --git a/ports/cpp/cmake/FindAntlr4Cpp.cmake b/ports/cpp/cmake/FindAntlr4Cpp.cmake new file mode 100644 index 0000000..d92bdcc --- /dev/null +++ b/ports/cpp/cmake/FindAntlr4Cpp.cmake @@ -0,0 +1,6 @@ +set(ANTLR4_WITH_STATIC_CRT OFF) +set(ANTLR4_ZIP_REPOSITORY https://github.com/antlr/antlr4/archive/refs/tags/${ANTLR4_TAG}.zip) +add_definitions(-DANTLR4CPP_STATIC) +include(ExternalAntlr4Cpp) + +set(ANTLR4C3_ANTLR4_STATIC antlr4_static) diff --git a/ports/cpp/cmake/FindAntlr4Tool.cmake b/ports/cpp/cmake/FindAntlr4Tool.cmake new file mode 100644 index 0000000..c832302 --- /dev/null +++ b/ports/cpp/cmake/FindAntlr4Tool.cmake @@ -0,0 +1,18 @@ +file( + DOWNLOAD + https://www.antlr.org/download/antlr-${ANTLR4_TAG}-complete.jar + ${CMAKE_BINARY_DIR}/antlr-${ANTLR4_TAG}-complete.jar +) +set(ANTLR_EXECUTABLE ${CMAKE_BINARY_DIR}/antlr-${ANTLR4_TAG}-complete.jar) + +function(antlr_generate grammar directory) + message(STATUS "antlr_generate ${grammar} ${directory}") + execute_process( + COMMAND bash -c "java \ + -jar ${ANTLR_EXECUTABLE} \ + -o ${directory} \ + -listener -visitor -Dlanguage=Cpp \ + ${grammar} \ + ${grammar}" + ) +endfunction() diff --git a/ports/cpp/cmake/FindGTest.cmake b/ports/cpp/cmake/FindGTest.cmake new file mode 100644 index 0000000..88c3d79 --- /dev/null +++ b/ports/cpp/cmake/FindGTest.cmake @@ -0,0 +1,11 @@ +include(FetchContent) + +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip +) + +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) +include(GoogleTest) diff --git a/ports/cpp/cmake/Sanitize.cmake b/ports/cpp/cmake/Sanitize.cmake new file mode 100644 index 0000000..e0f9bb7 --- /dev/null +++ b/ports/cpp/cmake/Sanitize.cmake @@ -0,0 +1,11 @@ +set( + CMAKE_CXX_FLAGS_ASAN "-g -fsanitize=address,undefined -fno-sanitize-recover=all" + CACHE STRING "Compiler flags in ASan build" + FORCE +) + +set( + CMAKE_CXX_FLAGS_TSAN "-g -fsanitize=thread -fno-sanitize-recover=all" + CACHE STRING "Compiler flags in TSan build" + FORCE +) diff --git a/ports/cpp/cmake/Testing.cmake b/ports/cpp/cmake/Testing.cmake new file mode 100644 index 0000000..fb2c5ef --- /dev/null +++ b/ports/cpp/cmake/Testing.cmake @@ -0,0 +1,29 @@ +macro(define_grammar_test grammar) + antlr_generate( + ${CMAKE_CURRENT_LIST_DIR}/${grammar} + ${CMAKE_CURRENT_BINARY_DIR} + ) + + file( + GLOB_RECURSE SOURCE CONFIGURE_DEPENDS + *.hpp *.cpp + ${CMAKE_CURRENT_BINARY_DIR}/*.hpp + ${CMAKE_CURRENT_BINARY_DIR}/*.cpp + ) + + add_executable(${PROJECT_NAME}-test ${SOURCE}) + + target_include_directories( + ${PROJECT_NAME}-test PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} + ) + + target_link_libraries( + ${PROJECT_NAME}-test PRIVATE + ${PROJECT_NAME} + GTest::gtest_main + GTest::gmock + ) + + gtest_discover_tests(${PROJECT_NAME}-test) +endmacro() diff --git a/ports/cpp/source/CMakeLists.txt b/ports/cpp/source/CMakeLists.txt new file mode 100644 index 0000000..5befee0 --- /dev/null +++ b/ports/cpp/source/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(${PROJECT_NAME} ${PROJECT_NAME}/CodeCompletionCore.cpp) +target_include_directories(${PROJECT_NAME} PUBLIC .) +target_link_libraries( + ${PROJECT_NAME} PUBLIC + ${ANTLR4C3_ANTLR4_STATIC} +) diff --git a/ports/cpp/CodeCompletionCore.cpp b/ports/cpp/source/antlr4-c3/CodeCompletionCore.cpp similarity index 99% rename from ports/cpp/CodeCompletionCore.cpp rename to ports/cpp/source/antlr4-c3/CodeCompletionCore.cpp index 41ea435..98599b6 100644 --- a/ports/cpp/CodeCompletionCore.cpp +++ b/ports/cpp/source/antlr4-c3/CodeCompletionCore.cpp @@ -188,7 +188,7 @@ bool CodeCompletionCore::translateStackToRuleIndex(RuleWithStartTokenList const& if (translateRulesTopDown) { // Loop over the rule stack from lowest to highest rule level. This will prioritize a lower preferred rule // if it is a child of a higher one that is also a preferred rule. - for (size_t i = ruleWithStartTokenList.size() - 1; i >= 0; i--) { + for (int64_t i = ruleWithStartTokenList.size() - 1; i >= 0; i--) { if (translateToRuleIndex(i, ruleWithStartTokenList)) { return true; } @@ -335,8 +335,8 @@ FollowSetsHolder CodeCompletionCore::determineFollowSets(antlr4::atn::ATNState * return { .sets = sets, + .combined = combined, .isExhaustive = isExhaustive, - .combined = combined }; } diff --git a/ports/cpp/CodeCompletionCore.hpp b/ports/cpp/source/antlr4-c3/CodeCompletionCore.hpp similarity index 100% rename from ports/cpp/CodeCompletionCore.hpp rename to ports/cpp/source/antlr4-c3/CodeCompletionCore.hpp diff --git a/ports/cpp/test/CMakeLists.txt b/ports/cpp/test/CMakeLists.txt new file mode 100644 index 0000000..74efe32 --- /dev/null +++ b/ports/cpp/test/CMakeLists.txt @@ -0,0 +1,5 @@ +# ANTLR4 Tool generates such files +add_compile_options(-Wno-unused-parameter) + +enable_testing() +add_subdirectory(expr) diff --git a/ports/cpp/test/expr/CMakeLists.txt b/ports/cpp/test/expr/CMakeLists.txt new file mode 100644 index 0000000..b02a3df --- /dev/null +++ b/ports/cpp/test/expr/CMakeLists.txt @@ -0,0 +1 @@ +define_grammar_test(Expr.g4) diff --git a/ports/cpp/test/expr/Expr.g4 b/ports/cpp/test/expr/Expr.g4 new file mode 100644 index 0000000..4dcbc5b --- /dev/null +++ b/ports/cpp/test/expr/Expr.g4 @@ -0,0 +1,74 @@ +grammar Expr; + +// $antlr-format columnLimit 100, minEmptyLines 1, maxEmptyLinesToKeep 1, useTab false +// $antlr-format reflowComments false, breakBeforeBraces false +// $antlr-format keepEmptyLinesAtTheStartOfBlocks false, allowShortRulesOnASingleLine false +// $antlr-format alignSemicolons hanging, alignColons hanging, alignTrailingComments true + +expression + : assignment + | simpleExpression + ; + +assignment + : (VAR | LET) ID EQUAL simpleExpression + ; + +simpleExpression + : simpleExpression (PLUS | MINUS) simpleExpression + | simpleExpression (MULTIPLY | DIVIDE) simpleExpression + | variableRef + | functionRef + ; + +variableRef + : ID + ; + +functionRef + : ID OPEN_PAR CLOSE_PAR + ; + +VAR + : [vV] [aA] [rR] + ; + +LET + : [lL] [eE] [tT] + ; + +PLUS + : '+' + ; + +MINUS + : '-' + ; + +MULTIPLY + : '*' + ; + +DIVIDE + : '/' + ; + +EQUAL + : '=' + ; + +OPEN_PAR + : '(' + ; + +CLOSE_PAR + : ')' + ; + +ID + : [a-zA-Z] [a-zA-Z0-9_]* + ; + +WS + : [ \n\r\t] -> channel(HIDDEN) + ; diff --git a/ports/cpp/test/expr/ExprTest.cpp b/ports/cpp/test/expr/ExprTest.cpp new file mode 100644 index 0000000..4fb8f2e --- /dev/null +++ b/ports/cpp/test/expr/ExprTest.cpp @@ -0,0 +1,275 @@ +#include +#include + +#include +#include + +#include +#include + +#include +#include + +class CountingErrorListener final : public antlr4::BaseErrorListener { +public: + void syntaxError(antlr4::Recognizer *recognizer, + antlr4::Token *offendingSymbol, std::size_t line, + std::size_t charPositionInLine, const std::string &msg, + std::exception_ptr e) override {} + + std::size_t GetErrorCount() const { return errorCount; } + +private: + std::size_t errorCount = 0; +}; + +struct AntlrPipeline { + AntlrPipeline(std::string_view text) + : chars(text), lexer(&chars), tokens(&lexer), parser(&tokens) {} + + antlr4::ANTLRInputStream chars; + ExprLexer lexer; + antlr4::CommonTokenStream tokens; + ExprParser parser; +}; + +template std::vector Keys(const std::map &map) { + std::vector keys; + for (const auto &[key, value] : map) { + keys.emplace_back(key); + } + return keys; +} + +using testing::ElementsAre; +using testing::UnorderedElementsAre; + +TEST(SimpleExpressionParser, MostSimpleSetup) { + CountingErrorListener listener; + + AntlrPipeline pipeline("var c = a + b()"); + pipeline.parser.addErrorListener(&listener); + + pipeline.parser.expression(); + EXPECT_EQ(listener.GetErrorCount(), 0); + + c3::CodeCompletionCore completion(&pipeline.parser); + const auto collectCandidatesAt = [&](std::size_t tokenIndex) { + return completion.collectCandidates(tokenIndex, + /*context=*/nullptr, /*size_t=*/0, + /*cancel=*/nullptr); + }; + + { + // 1) At the input start. + auto candidates = collectCandidatesAt(0); + EXPECT_THAT( + Keys(candidates.tokens), + UnorderedElementsAre(ExprLexer::VAR, ExprLexer::LET, ExprLexer::ID)); + EXPECT_THAT(candidates.tokens[ExprLexer::VAR], + ElementsAre(ExprLexer::ID, ExprLexer::EQUAL)); + EXPECT_THAT(candidates.tokens[ExprLexer::LET], + ElementsAre(ExprLexer::ID, ExprLexer::EQUAL)); + EXPECT_THAT(candidates.tokens[ExprLexer::ID], ElementsAre()); + } + { + // 2) On the first whitespace. In real implementations you would do some + // additional checks where in the whitespace the caret is, as the outcome is + // different depending on that position. + auto candidates = collectCandidatesAt(1); + EXPECT_THAT(Keys(candidates.tokens), UnorderedElementsAre(ExprLexer::ID)); + } + { + // 3) On the variable name ('c'). + auto candidates = collectCandidatesAt(2); + EXPECT_THAT(Keys(candidates.tokens), UnorderedElementsAre(ExprLexer::ID)); + } + { + // 4) On the equal sign (ignoring whitespace positions from now on). + auto candidates = collectCandidatesAt(4); + EXPECT_THAT(Keys(candidates.tokens), + UnorderedElementsAre(ExprLexer::EQUAL)); + } + { + // 5) On the variable reference 'a'. But since we have not configure the c3 + // engine to return us var refs (or function refs for that matter) we only + // get an ID here. + auto candidates = collectCandidatesAt(6); + EXPECT_THAT(Keys(candidates.tokens), UnorderedElementsAre(ExprLexer::ID)); + } + { + // 6) On the '+' operator. Usually you would not show operators as + // candidates, but we have not set up the c3 engine yet to not return them. + auto candidates = collectCandidatesAt(8); + EXPECT_THAT(Keys(candidates.tokens), + UnorderedElementsAre(ExprLexer::PLUS, ExprLexer::MINUS, + ExprLexer::MULTIPLY, ExprLexer::DIVIDE, + ExprLexer::OPEN_PAR)); + } +} + +TEST(SimpleExpressionParser, TypicalSetup) { + CountingErrorListener listener; + + AntlrPipeline pipeline("var c = a + b()"); + pipeline.parser.addErrorListener(&listener); + + pipeline.parser.expression(); + EXPECT_EQ(listener.GetErrorCount(), 0); + + c3::CodeCompletionCore completion(&pipeline.parser); + completion.ignoredTokens = { + ExprLexer::ID, ExprLexer::PLUS, ExprLexer::MINUS, + ExprLexer::MULTIPLY, ExprLexer::DIVIDE, ExprLexer::EQUAL, + }; + completion.preferredRules = { + ExprParser::RuleFunctionRef, + ExprParser::RuleVariableRef, + }; + + const auto collectCandidatesAt = [&](std::size_t tokenIndex) { + return completion.collectCandidates(tokenIndex, + /*context=*/nullptr, /*size_t=*/0, + /*cancel=*/nullptr); + }; + + { + // 1) At the input start. + auto candidates = collectCandidatesAt(0); + EXPECT_THAT(Keys(candidates.tokens), + UnorderedElementsAre(ExprLexer::VAR, ExprLexer::LET)); + + // NOTE: Behaviour differs from TypeScript version + EXPECT_THAT(candidates.tokens[ExprLexer::VAR], UnorderedElementsAre()); + EXPECT_THAT(candidates.tokens[ExprLexer::LET], UnorderedElementsAre()); + } + { + // 2) On the variable name ('c'). + auto candidates = collectCandidatesAt(2); + EXPECT_EQ(candidates.tokens.size(), 0); + } + { + // 4) On the equal sign. + auto candidates = collectCandidatesAt(4); + EXPECT_EQ(candidates.tokens.size(), 0); + } + { + // 5) On the variable reference 'a'. + auto candidates = collectCandidatesAt(6); + EXPECT_EQ(candidates.tokens.size(), 0); + // Here we get 2 rule indexes, derived from 2 different IDs possible at this + // caret position. These are what we told the engine above to be preferred + // rules for us. + EXPECT_THAT(Keys(candidates.rules), + UnorderedElementsAre(ExprParser::RuleFunctionRef, + ExprParser::RuleVariableRef)); + EXPECT_EQ(candidates.rules[ExprParser::RuleFunctionRef].startTokenIndex, 6); + EXPECT_EQ(candidates.rules[ExprParser::RuleVariableRef].startTokenIndex, 6); + } + { + // 6) On the whitespace just after the variable reference 'a' (but it could + // still be a function reference!). + auto candidates = collectCandidatesAt(7); + EXPECT_EQ(candidates.tokens.size(), 0); + EXPECT_THAT(Keys(candidates.rules), + UnorderedElementsAre(ExprParser::RuleFunctionRef)); + EXPECT_EQ(candidates.rules[ExprParser::RuleFunctionRef].startTokenIndex, 6); + } +} + +TEST(SimpleExpressionParser, RecursivePreferredRule) { + AntlrPipeline pipeline("var c = a + b"); + + CountingErrorListener listener; + pipeline.parser.addErrorListener(&listener); + pipeline.parser.expression(); + EXPECT_EQ(listener.GetErrorCount(), 0); + + c3::CodeCompletionCore completion(&pipeline.parser); + completion.preferredRules = {ExprParser::RuleSimpleExpression}; + + const auto collectCandidatesAt = [&](std::size_t tokenIndex) { + return completion.collectCandidates(tokenIndex, + /*context=*/nullptr, /*size_t=*/0, + /*cancel=*/nullptr); + }; + + { + // 1) On the variable reference 'a'. + auto candidates = collectCandidatesAt(6); + EXPECT_THAT(Keys(candidates.rules), + UnorderedElementsAre(ExprParser::RuleSimpleExpression)); + // The start token of the simpleExpression rule begins at token 'a'. + EXPECT_EQ( + candidates.rules[ExprParser::RuleSimpleExpression].startTokenIndex, 6); + } + { + // 2) On the variable reference 'b'. + completion.translateRulesTopDown = false; + auto candidates = collectCandidatesAt(10); + EXPECT_THAT(Keys(candidates.rules), + UnorderedElementsAre(ExprParser::RuleSimpleExpression)); + // When translateRulesTopDown is false, startTokenIndex should match the + // start token for the lower index (less specific) rule in the expression, + // which is 'a'. + EXPECT_EQ( + candidates.rules[ExprParser::RuleSimpleExpression].startTokenIndex, 6); + } + { + // 3) On the variable reference 'b' topDown preferred rules. + completion.translateRulesTopDown = true; + auto candidates = collectCandidatesAt(10); + EXPECT_THAT(Keys(candidates.rules), + UnorderedElementsAre(ExprParser::RuleSimpleExpression)); + // When translateRulesTopDown is true, startTokenIndex should match the + // start token for the higher index (more specific) rule in the expression, + // which is 'b'. + EXPECT_EQ( + candidates.rules[ExprParser::RuleSimpleExpression].startTokenIndex, 10); + } +} + +TEST(SimpleExpressionParser, CandidateRulesWithDifferentStartTokens) { + AntlrPipeline pipeline("var c = a + b"); + + CountingErrorListener listener; + pipeline.parser.addErrorListener(&listener); + pipeline.parser.expression(); + EXPECT_EQ(listener.GetErrorCount(), 0); + + c3::CodeCompletionCore completion(&pipeline.parser); + completion.preferredRules = { + ExprParser::RuleAssignment, + ExprParser::RuleVariableRef, + }; + completion.translateRulesTopDown = true; + + const auto collectCandidatesAt = [&](std::size_t tokenIndex) { + return completion.collectCandidates(tokenIndex, + /*context=*/nullptr, /*size_t=*/0, + /*cancel=*/nullptr); + }; + + { + // 1) On the token 'var'. + auto candidates = collectCandidatesAt(0); + EXPECT_THAT(Keys(candidates.rules), + UnorderedElementsAre(ExprParser::RuleAssignment, + ExprParser::RuleVariableRef)); + // The start token of the assignment and variableRef rules begin at token + // 'var'. + EXPECT_EQ(candidates.rules[ExprParser::RuleAssignment].startTokenIndex, 0); + EXPECT_EQ(candidates.rules[ExprParser::RuleVariableRef].startTokenIndex, 0); + } + { + // 2) On the variable reference 'a'. + auto candidates = collectCandidatesAt(6); + EXPECT_THAT(Keys(candidates.rules), + UnorderedElementsAre(ExprParser::RuleAssignment, + ExprParser::RuleVariableRef)); + // The start token of the assignment rule begins at token 'var'. + EXPECT_EQ(candidates.rules[ExprParser::RuleAssignment].startTokenIndex, 0); + // The start token of the variableRef rule begins at token 'a'. + EXPECT_EQ(candidates.rules[ExprParser::RuleVariableRef].startTokenIndex, 6); + } +} \ No newline at end of file