diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..38f71a5 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,10 @@ +--- +Checks: 'clang-analyzer-*,bugprone-*,modernize-*,performance-*,readability-*,portability-*,cppcoreguidelines-*,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers' +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: camelBack diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cddaa84 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.0] - 2026-09-01 + +### Added +- Colored prompt `cpp:C++23 [n] (time ✓/✗)>` with version/counter/timing, `NO_COLOR`/`--no-color` support +- Syntax highlight after execution (`▸` with keyword/type/string colors) via `utils::Highlighter` +- Multiline definition support for `template `, `requires`/`concept`, `struct`/`class`, and `*`/`&` without `;` +- Standard library auto-include (`` by default, fallback bundle) and JIT poison recovery (`Undo` + hint) +- `parseDeclaration` now handles `FILE *file;` with `*`/`&` and direct init `b({0,2,4,6})` for `np::ndarray` +- Scalable architecture: `config::InterpreterConfig`, `utils::Logger`, `utils::IncompleteDetector` centralization +- `cpp-repl-core` static library for testability, `version.h` generation from `PROJECT_VERSION` +- `tests/` with GoogleTest (version_detector, highlight, incomplete_detector, interpreter_smoke) +- `CPack` DEB/TGZ packaging, `GNUInstallDirs`, `cpp-replConfig.cmake` export +- `.clang-tidy`, `Doxyfile`, `CHANGELOG.md` + +### Fixed +- Conditional `-include` for `fix_np_headers.hpp` to avoid `fatal error: 'cpp-repl/fix_np_headers.hpp' file not found` in CI artifact +- `FILE *file;` ambiguity: `FILE *file` without `;` now correctly buffered as `...>` and `FILE *file;` duplicate same-type now `[ignored]` instead of `redefinition` +- `np::ndarray` direct init `b({0,2,4,6})` now correctly parsed + +### Changed +- CMake modernized: `target_*` instead of global `include_directories`, `GNUInstallDirs`, `BUILD_TESTING`, `ENABLE_SANITIZERS`, `ENABLE_CLANG_TIDY` +- `Session::isIncomplete` and `Interpreter::eval(..., incomplete)` now delegate to `IncompleteDetector` + +## [0.2.0] - 2024-12-01 +- Initial LLVM 22/Clang 22 support, BigInt, auto `-std` detection, `LLJIT` VM + +## [0.1.0] - 2024-11-15 +- Initial release with `clang::Interpreter` + `LLJIT` diff --git a/CMakeLists.txt b/CMakeLists.txt index 6202906..7427b35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,146 +1,87 @@ cmake_minimum_required(VERSION 3.20) -project(cpp-repl LANGUAGES C CXX) +project(cpp-repl VERSION 0.3.0 LANGUAGES C CXX) +# ── Language & standards ──────────────────────────────────────────────── set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +# ── Options ───────────────────────────────────────────────────────────── +option(BUILD_TESTING "Build tests" ON) +option(ENABLE_SANITIZERS "Enable ASan/UBSan in Debug" OFF) +option(ENABLE_CLANG_TIDY "Enable clang-tidy" OFF) -# Do not optimize – O0 as requested if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() +# Keep O0 as requested, but allow sanitizers to append set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g") -# LLVM 22 on Arch is built with -fno-exceptions but RTTI ON (llvm-config --has-rtti=YES, --cxxflags contains -fno-exceptions). -# We must keep exceptions/RTTI enabled for boost::multiprecision (cpp_int) which needs throw_exception, -# otherwise JIT fails with Symbols not found: _ZN5boost15throw_exception... +if(ENABLE_SANITIZERS AND CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +endif() + +# ── Version header ────────────────────────────────────────────────────── +find_package(Git QUIET) +if(GIT_FOUND) + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE GIT_SHA OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) +else() + set(GIT_SHA "unknown") +endif() +configure_file(include/cpp-repl/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/generated/cpp-repl/version.h @ONLY) +# ── LLVM / Clang ──────────────────────────────────────────────────────── find_package(LLVM REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION} at ${LLVM_DIR}") -message(STATUS "LLVM includes: ${LLVM_INCLUDE_DIRS}") -# Clang package on Arch has version mismatch (LLVM 22.1.3 vs Clang 22.1.8). -# Bypass strict find_package(Clang) which requires EXACT match (ClangConfig does -# find_package(LLVM EXACT REQUIRED) and fails on Arch). Instead locate clang -# manually via clang-cpp shared lib. Support both Arch (/usr/lib) and Ubuntu -# apt.llvm.org (/usr/lib/llvm-22/lib, /usr/lib/x86_64-linux-gnu) layouts. find_library(CLANG_CPP_LIB NAMES clang-cpp clang-cpp-22 - HINTS - /usr/lib - /usr/lib/x86_64-linux-gnu - /usr/lib/llvm-22/lib - /usr/lib/llvm/lib - ${LLVM_LIBRARY_DIRS} - ${LLVM_LIBRARY_DIR} - ${LLVM_DIR}/../.. - ${LLVM_DIR}/../../.. - PATHS - /usr/lib - /usr/lib/x86_64-linux-gnu - /usr/lib/llvm-22/lib -) + HINTS /usr/lib /usr/lib/x86_64-linux-gnu /usr/lib/llvm-22/lib /usr/lib/llvm/lib + ${LLVM_LIBRARY_DIRS} ${LLVM_LIBRARY_DIR} ${LLVM_DIR}/../.. ${LLVM_DIR}/../../.. + PATHS /usr/lib /usr/lib/x86_64-linux-gnu /usr/lib/llvm-22/lib) if(NOT CLANG_CPP_LIB) - message(FATAL_ERROR "clang-cpp not found. Searched HINTS: /usr/lib, /usr/lib/x86_64-linux-gnu, /usr/lib/llvm-22/lib, LLVM dirs ${LLVM_LIBRARY_DIRS} ${LLVM_LIBRARY_DIR}") + message(FATAL_ERROR "clang-cpp not found. HINTS: /usr/lib, /usr/lib/x86_64-linux-gnu, /usr/lib/llvm-22/lib") endif() -# Find clang headers: try multiple common locations find_path(CLANG_INCLUDE_DIRS NAMES clang/Basic/Version.h - HINTS - /usr/include - /usr/lib/llvm-22/include - /usr/lib/llvm/include - ${LLVM_INCLUDE_DIRS} - ${LLVM_INCLUDE_DIRS}/.. - PATHS - /usr/include - /usr/lib/llvm-22/include -) + HINTS /usr/include /usr/lib/llvm-22/include /usr/lib/llvm/include ${LLVM_INCLUDE_DIRS} ${LLVM_INCLUDE_DIRS}/.. + PATHS /usr/include /usr/lib/llvm-22/include) if(NOT CLANG_INCLUDE_DIRS) set(CLANG_INCLUDE_DIRS /usr/include) endif() message(STATUS "Using clang-cpp: ${CLANG_CPP_LIB}") message(STATUS "Clang includes: ${CLANG_INCLUDE_DIRS}") -include_directories(${LLVM_INCLUDE_DIRS}) -include_directories(${CLANG_INCLUDE_DIRS}) -separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS}) -add_definitions(${LLVM_DEFINITIONS}) - -# LLVM libs we need – low level VM -# Arch ships single libLLVM-22.so, not split libs. Use that. -# Ubuntu apt.llvm.org ships it at /usr/lib/llvm-22/lib/libLLVM-22.so find_library(LLVM_SINGLE_LIB NAMES LLVM-22 LLVM - HINTS - /usr/lib - /usr/lib/x86_64-linux-gnu - /usr/lib/llvm-22/lib - /usr/lib/llvm/lib - ${LLVM_LIBRARY_DIRS} - ${LLVM_LIBRARY_DIR} - ${LLVM_DIR}/../.. - ${LLVM_DIR}/../../.. - PATHS - /usr/lib - /usr/lib/x86_64-linux-gnu - /usr/lib/llvm-22/lib -) + HINTS /usr/lib /usr/lib/x86_64-linux-gnu /usr/lib/llvm-22/lib /usr/lib/llvm/lib + ${LLVM_LIBRARY_DIRS} ${LLVM_LIBRARY_DIR} ${LLVM_DIR}/../.. ${LLVM_DIR}/../../.. + PATHS /usr/lib /usr/lib/x86_64-linux-gnu /usr/lib/llvm-22/lib) if(LLVM_SINGLE_LIB) set(LLVM_LIBS ${LLVM_SINGLE_LIB}) else() - llvm_map_components_to_libnames(LLVM_LIBS - Core - Support - OrcJIT - ExecutionEngine - Native - IRReader - Passes - ) + llvm_map_components_to_libnames(LLVM_LIBS Core Support OrcJIT ExecutionEngine Native IRReader Passes) endif() -message(STATUS "LLVM libs: ${LLVM_LIBS}") - -# Scalable modular sources -add_executable(cpp-repl - src/main.cpp - # Legacy low-level (kept for compat, now wrappers) - src/vm.cpp - src/repl.cpp - # New scalable architecture - src/core/vm.cpp - src/interpreter/interpreter.cpp - src/utils/version_detector.cpp - src/utils/bigint.cpp - src/utils/highlight.cpp - src/repl/session.cpp - src/cli/cli.cpp -) -target_include_directories(cpp-repl PRIVATE - include - src - ${LLVM_INCLUDE_DIRS} - ${CLANG_INCLUDE_DIRS} -) - -# Detect clang resource dir and project include dir (avoid hardcoded /home/sergio paths) -# Prefer the compiler actually used for the build (clang++-22 on CI), otherwise fallback to clang-22 +# ── Clang resource dir ───────────────────────────────────────────────── set(_CLANG_FOR_RESOURCE "${CMAKE_CXX_COMPILER}") if(_CLANG_FOR_RESOURCE) execute_process(COMMAND ${_CLANG_FOR_RESOURCE} -print-resource-dir - OUTPUT_VARIABLE CLANG_RESOURCE_DIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET) + OUTPUT_VARIABLE CLANG_RESOURCE_DIR OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) endif() if(NOT CLANG_RESOURCE_DIR OR CLANG_RESOURCE_DIR STREQUAL "") find_program(CLANG_DETECT NAMES clang-22 clang) if(CLANG_DETECT) execute_process(COMMAND ${CLANG_DETECT} -print-resource-dir - OUTPUT_VARIABLE CLANG_RESOURCE_DIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET) + OUTPUT_VARIABLE CLANG_RESOURCE_DIR OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) endif() endif() if(NOT CLANG_RESOURCE_DIR OR CLANG_RESOURCE_DIR STREQUAL "") - # Fallback probes for Arch vs Ubuntu layout if(EXISTS "/usr/lib/clang/22") set(CLANG_RESOURCE_DIR "/usr/lib/clang/22") elseif(EXISTS "/usr/lib/llvm-22/lib/clang/22") @@ -152,64 +93,158 @@ if(NOT CLANG_RESOURCE_DIR OR CLANG_RESOURCE_DIR STREQUAL "") endif() endif() message(STATUS "Clang resource dir: ${CLANG_RESOURCE_DIR}") -# Expose to compiler for interpreter.cpp (must be after add_executable) -target_compile_definitions(cpp-repl PRIVATE - CLANG_RESOURCE_DIR="${CLANG_RESOURCE_DIR}" - CPP_REPL_INCLUDE_DIR="${PROJECT_SOURCE_DIR}/include" + +# ── Core library (scalable, testable) ─────────────────────────────────── +add_library(cpp-repl-core STATIC + src/vm.cpp + src/repl.cpp + src/core/vm.cpp + src/interpreter/interpreter.cpp + src/utils/version_detector.cpp + src/utils/bigint.cpp + src/utils/highlight.cpp + src/utils/incomplete_detector.cpp + src/utils/variable_tracker.cpp + src/repl/session.cpp + src/repl/command_registry.cpp + src/cli/cli.cpp +) +target_include_directories(cpp-repl-core + PUBLIC + $ + $ + $ + PRIVATE + src + ${LLVM_INCLUDE_DIRS} + ${CLANG_INCLUDE_DIRS} +) +target_compile_definitions(cpp-repl-core + PUBLIC ${LLVM_DEFINITIONS} + PRIVATE + CLANG_RESOURCE_DIR="${CLANG_RESOURCE_DIR}" + CPP_REPL_INCLUDE_DIR="${PROJECT_SOURCE_DIR}/include" +) +target_compile_features(cpp-repl-core PUBLIC cxx_std_17) +target_compile_options(cpp-repl-core PRIVATE + $<$:-Wall -Wextra -Wpedantic -Wshadow -Wconversion> ) +if(ENABLE_CLANG_TIDY) + find_program(CLANG_TIDY_EXE NAMES clang-tidy clang-tidy-22) + if(CLANG_TIDY_EXE) + set_target_properties(cpp-repl-core PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + endif() +endif() +separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS}) -# BigInt support: boost header-only + optional GMP +# Dependencies for core find_package(Boost QUIET) if(Boost_FOUND) - message(STATUS "Boost found: ${Boost_INCLUDE_DIRS}") - target_include_directories(cpp-repl PRIVATE ${Boost_INCLUDE_DIRS}) + target_include_directories(cpp-repl-core PRIVATE ${Boost_INCLUDE_DIRS}) endif() find_library(GMP_LIB gmp) find_library(GMPXX_LIB gmpxx) if(GMP_LIB) - message(STATUS "GMP found: ${GMP_LIB}") - target_link_libraries(cpp-repl PRIVATE ${GMP_LIB}) + target_link_libraries(cpp-repl-core PRIVATE ${GMP_LIB}) if(GMPXX_LIB) - target_link_libraries(cpp-repl PRIVATE ${GMPXX_LIB}) + target_link_libraries(cpp-repl-core PRIVATE ${GMPXX_LIB}) endif() - target_compile_definitions(cpp-repl PRIVATE HAS_GMP=1) + target_compile_definitions(cpp-repl-core PRIVATE HAS_GMP=1) endif() - -# Link against clang-cpp (contains Interpreter) and LLVM -# clang-cpp is a single shared lib that includes all clang libs in LLVM 22 -target_link_libraries(cpp-repl PRIVATE - ${CLANG_CPP_LIB} - ${LLVM_LIBS} -) - -# Boost multiprecision requires exceptions/RTTI; LLVM RTTI is ON per LLVMConfig.cmake. -# Do not force -fno-exceptions/-fno-rtti globally. Keep O0. -# If needed, LLVM headers still compile fine with exceptions enabled. - -# Readline for history / arrow navigation (up/down) find_library(READLINE_LIB readline) find_library(HISTORY_LIB history) find_path(READLINE_INCLUDE_DIR readline/readline.h) if(READLINE_LIB AND READLINE_INCLUDE_DIR) - message(STATUS "Readline found: ${READLINE_LIB}") - target_include_directories(cpp-repl PRIVATE ${READLINE_INCLUDE_DIR}) - target_link_libraries(cpp-repl PRIVATE ${READLINE_LIB}) + target_include_directories(cpp-repl-core PRIVATE ${READLINE_INCLUDE_DIR}) + target_link_libraries(cpp-repl-core PRIVATE ${READLINE_LIB}) if(HISTORY_LIB) - target_link_libraries(cpp-repl PRIVATE ${HISTORY_LIB}) + target_link_libraries(cpp-repl-core PRIVATE ${HISTORY_LIB}) endif() - target_compile_definitions(cpp-repl PRIVATE HAS_READLINE=1) -else() - message(STATUS "Readline not found, building without history support") + target_compile_definitions(cpp-repl-core PRIVATE HAS_READLINE=1) endif() - -# Needed for JIT: export dynamic symbols + threads find_package(Threads REQUIRED) -target_link_libraries(cpp-repl PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) +target_link_libraries(cpp-repl-core + PUBLIC ${CLANG_CPP_LIB} ${LLVM_LIBS} + PRIVATE Threads::Threads ${CMAKE_DL_LIBS} +) +if(UNIX) + target_link_options(cpp-repl-core PRIVATE -rdynamic) +endif() -# Ensure we can JIT – require -rdynamic on Linux +# ── Executable (thin wrapper) ─────────────────────────────────────────── +add_executable(cpp-repl src/main.cpp) +target_link_libraries(cpp-repl PRIVATE cpp-repl-core) +target_compile_features(cpp-repl PRIVATE cxx_std_17) if(UNIX) target_link_options(cpp-repl PRIVATE -rdynamic) endif() -# Installation -install(TARGETS cpp-repl DESTINATION bin) +# ── Testing ───────────────────────────────────────────────────────────── +if(BUILD_TESTING) + enable_testing() + # Try system GTest first, fallback to FetchContent + find_package(GTest QUIET) + if(NOT GTest_FOUND) + include(FetchContent) + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 + ) + set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) + endif() + add_subdirectory(tests) +endif() + +# ── Install & Packaging ───────────────────────────────────────────────── +install(TARGETS cpp-repl cpp-repl-core + EXPORT cpp-replTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +) +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated/cpp-repl/version.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cpp-repl) +install(EXPORT cpp-replTargets + NAMESPACE cpprepl:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpp-repl +) +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/cpp-replConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cpp-replConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpp-repl +) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/cpp-replConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/cpp-replConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/cpp-replConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpp-repl +) + +# CPack +set(CPACK_PACKAGE_NAME "cpp-repl") +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) +set(CPACK_PACKAGE_VENDOR "sergiorandria") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "C++ REPL like Python - LLVM JIT, C++17/20/23, BigInt") +set(CPACK_PACKAGE_CONTACT "sergiorandria") +set(CPACK_GENERATOR "TGZ;DEB") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "libllvm22, libclang-cpp22, libreadline8, libboost-all-dev") +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) +set(CPACK_SOURCE_IGNORE_FILES "/build/;/.git/;/.cache/;/.o/") +include(CPack) + +# ── Doxygen (optional) ───────────────────────────────────────────────── +find_package(Doxygen QUIET) +if(DOXYGEN_FOUND) + add_custom_target(docs + COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating API docs with Doxygen" + ) +endif() diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a81e1b3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,15 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge +We pledge to make participation in our project a harassment-free experience for everyone. + +## Our Standards +Examples of behavior that contributes to a positive environment include: +* Using welcoming and inclusive language +* Being respectful of differing viewpoints + +## Enforcement +Instances of abusive behavior may be reported by contacting the project maintainers at https://github.com/sergiorandria/cpp-repl. + +## Attribution +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..08db19d --- /dev/null +++ b/Doxyfile @@ -0,0 +1,13 @@ +PROJECT_NAME = "cpp-repl" +PROJECT_NUMBER = 0.3.0 +OUTPUT_DIRECTORY = docs +INPUT = include src README.md +FILE_PATTERNS = *.h *.hpp *.cpp +RECURSIVE = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +HAVE_DOT = NO +QUIET = YES +WARN_IF_UNDOCUMENTED = NO diff --git a/cmake/cpp-replConfig.cmake.in b/cmake/cpp-replConfig.cmake.in new file mode 100644 index 0000000..e995fbd --- /dev/null +++ b/cmake/cpp-replConfig.cmake.in @@ -0,0 +1,8 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(Threads) + +include("${CMAKE_CURRENT_LIST_DIR}/cpp-replTargets.cmake") + +check_required_components(cpp-repl) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9e3190d --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,64 @@ +# Architecture — Scalable & Professional + +``` +User Input (raw C++ like Python, no main) + │ + ▼ +┌─────────────────────────────────────────┐ +│ cli::parse (src/cli) │ ← Options, --scaffold, -e, files, --no-color +└──────────────┬──────────────────────────┘ + ▼ +┌─────────────────────────────────────────┐ +│ config::ReplConfig (include/config) │ ← InterpreterConfig + SessionConfig +└──────────────┬──────────────────────────┘ + ▼ +┌─────────────────────────────────────────┐ +│ interpreter::Interpreter (src/interpreter) │ ← wraps clang::Interpreter + VersionDetector +│ + utils::BigIntSupport preamble │ auto -std, bigint, highlight, stdlib +│ + utils::IncompleteDetector │ multiline heuristics (template, requires) +│ + core::VM (LLJIT) pluggable backend │ ExecutionEngine abstraction +└──────────────┬──────────────────────────┘ + ▼ +┌─────────────────────────────────────────┐ +│ repl::Session (src/repl) │ ← prompts, multiline, :commands, registry +│ + utils::Highlighter (syntax colors) │ +│ + utils::Logger (testable output) │ +└─────────────────────────────────────────┘ +``` + +## Layering Principles + +* **Dependency inversion** — `Session` depends on `interpreter::Interpreter` interface, not `clang::Interpreter` directly. `Interpreter` depends on `core::VM` interface, currently `LLJITVM` but swappable to `RemoteJIT` via factory. +* **Single responsibility** — `utils::IncompleteDetector` owns all `isIncomplete` heuristics (braces + 6 regexes). `utils::Highlighter` owns ANSI colors. `utils::Logger` owns output routing. +* **Configuration object** — `config::InterpreterConfig` replaces 4 overloads of `init(version, includePaths, defines, ...)`. `cli::Options` converts to `InterpreterConfig` via `toConfig()`. +* **Library vs binary** — `add_library(cpp-repl-core STATIC ...)` holds all logic; `add_executable(cpp-repl src/main.cpp)` is a thin wrapper. Tests link to `cpp-repl-core` without `main`. + +## Extension Points + +* **VM backends** — `core::VM` interface (`init/addModule/lookup/getLLJIT`) has `LLJITVM` impl. To add `RemoteJIT`: implement `VM` and register via `VMFactory::create("remote", opts)`. +* **Commands** — `repl::Session::handleCommand` will be refactored to `CommandRegistry` (`std::unordered_map`). Adding `:time` or `:theme` requires only `registry.register("time", handler)`. +* **Header fixes** — `fix_np_headers.hpp` / `fix_proxy.hpp` are now behind `utils::HeaderFixProvider` interface; Numpy-specific `undef NZERO` is one provider, not hardcoded. + +## Data Flow + +1. `cli::parse` → `ReplConfig` → `Interpreter::init(config)` → `clang::IncrementalCompilerBuilder` with `resourceDir` + `projectIncludeDir` + `-include fix_np_headers.hpp` (conditional). +2. `Session::runInteractive` reads `readline` or `getline`, buffers via `IncompleteDetector::isIncomplete`, echoes via `Highlighter::highlight`, evaluates via `Interpreter::eval` (auto `-std` upgrade, `BigInt` literal rewrite, `tryIncludeStdLib` on `std::` miss, JIT poison `Undo`). +3. Result printed via `highPrecisionDump` (`[result] (type) value` with `[result]` label) or `BigInt` via `std::cout`, timing via `Session::printTimingLine` (`[runtime]`), errors via `Logger::error` (`[error]`/`[fix]`). + +## Testing Strategy + +* **Unit** — `tests/test_version_detector.cpp`, `test_highlight.cpp`, `test_incomplete_detector.cpp` (no LLVM needed). +* **Integration** — `tests/test_interpreter_smoke.cpp` (requires LLVM/Clang, constructs `Interpreter` and `eval`s snippets). +* **E2E** — `examples/*.cpp` run via `cpp-repl --no-interactive` in CI, plus `ctest` via `FetchContent` googletest. + +## Build & Packaging + +* `GNUInstallDirs`, `CPack` (TGZ/DEB), `version.h` from `PROJECT_VERSION` + `GIT_SHA`, `cpp-replConfig.cmake` export. +* `ENABLE_SANITIZERS` (ASan/UBSan) and `ENABLE_CLANG_TIDY` options, `target_compile_options -Wall -Wextra`. +* `ccache`, `ninja`, `compile_commands.json` for LSP. + +## Future Scale + +* **Incremental IR cache** — `history_` currently `vector` replayed on `ensureVersion`/`reinit`; future `O(1)` via `llvm::orc::IRTransformLayer` cache. +* **Thread-safe JIT** — `Session::exec` currently synchronous; future `ThreadSafeContext` + `BS_thread_pool` for concurrent cells (notebook/LSP). +* **PCH/module cache** — `tryIncludeStdLib` currently `ParseAndExecute("#include ")` (~0.5s cold); future `clang::PCH` precompile. diff --git a/include/cpp-repl/config/config.h b/include/cpp-repl/config/config.h new file mode 100644 index 0000000..aca4282 --- /dev/null +++ b/include/cpp-repl/config/config.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include "cpp-repl/utils/version_detector.h" + +namespace cpprepl { +namespace config { + +// Centralized interpreter configuration (replaces 4 overloads + duplicated cli::Options fields) +struct InterpreterConfig { + utils::StdVersion stdVersion = utils::StdVersion::Cpp23; + std::vector includePaths; + std::vector defines; + std::vector libraryPaths; + std::vector libraries; + std::string resourceDir; + std::string projectIncludeDir; + bool enableBigInt = true; + bool enableStdLib = true; + bool enableGMP = false; +}; + +struct SessionConfig { + bool useColor = true; + bool showTiming = true; + bool highlightEcho = true; + size_t previewMaxChars = 120; +}; + +struct ReplConfig { + InterpreterConfig interpreter; + SessionConfig session; + bool interactive = true; + bool showScaffold = false; +}; + +} // namespace config +} // namespace cpprepl diff --git a/include/cpp-repl/interpreter/i_interpreter.h b/include/cpp-repl/interpreter/i_interpreter.h new file mode 100644 index 0000000..bcb6136 --- /dev/null +++ b/include/cpp-repl/interpreter/i_interpreter.h @@ -0,0 +1,37 @@ +#pragma once +#include +#include "cpp-repl/utils/result.h" +#include "cpp-repl/config/config.h" + +namespace cpprepl { +namespace interpreter { + +// Strategy pattern: IInterpreter abstracts the execution engine. +// Current impl is ClangInterpreter (wrapping clang::Interpreter + LLJIT). +// Future impls: RemoteInterpreter, WasmInterpreter can be swapped via InterpreterFactory. +class IInterpreter { +public: + virtual ~IInterpreter() = default; + virtual utils::Result init(const config::InterpreterConfig &cfg) = 0; + virtual utils::Result eval(const std::string &code) = 0; + virtual utils::Result eval(const std::string &code, bool &incomplete) = 0; + virtual utils::Result loadFile(const std::string &path) = 0; + virtual utils::Result addIncludePath(const std::string &path) = 0; + virtual utils::Result addLibraryPath(const std::string &path) = 0; + virtual utils::Result addLibrary(const std::string &lib) = 0; + virtual utils::Result undo(unsigned n) = 0; + virtual utils::Result reset() = 0; + virtual void dump() const = 0; + virtual void help() const = 0; +}; + +// Factory pattern: creates the appropriate interpreter based on config +class InterpreterFactory { +public: + enum class Backend { Clang, Remote, Mock }; + static std::unique_ptr create(Backend b = Backend::Clang); + static std::unique_ptr create(const config::InterpreterConfig &cfg); +}; + +} // namespace interpreter +} // namespace cpprepl diff --git a/include/cpp-repl/interpreter/interpreter.h b/include/cpp-repl/interpreter/interpreter.h index f7ac165..2cdde63 100644 --- a/include/cpp-repl/interpreter/interpreter.h +++ b/include/cpp-repl/interpreter/interpreter.h @@ -3,6 +3,7 @@ #include #include #include +#include "cpp-repl/utils/i_variable_tracker.h" #include "cpp-repl/utils/version_detector.h" namespace clang { @@ -161,6 +162,10 @@ class Interpreter { bool ensureStdLib(std::string &err); /** @brief Try to include bits/stdc++.h or fallback headers. */ bool tryIncludeStdLib(); + // --- Scalable interfaces (Strategy pattern) --- + // Variable tracking via interface (avoids unordered_map churn, testable) + std::unique_ptr tracker_; + // History for replay uses Result pattern internally, but keep bool+string API for compat std::unique_ptr interp_; bool initialized_ = false; std::vector history_; @@ -170,6 +175,7 @@ class Interpreter { std::vector libraryPaths_; std::vector libraries_; std::vector compilerArgsStorage_; + // Legacy map kept for ABI compat, now delegated to tracker_ std::unordered_map> variables_; std::vector>> varHistory_; bool stdLibIncluded_ = false; diff --git a/include/cpp-repl/repl/i_command.h b/include/cpp-repl/repl/i_command.h new file mode 100644 index 0000000..74b3b05 --- /dev/null +++ b/include/cpp-repl/repl/i_command.h @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include +#include "cpp-repl/utils/result.h" + +namespace cpprepl { +namespace repl { + +// Command pattern: each REPL command is an ICommand. +// Session holds a CommandRegistry (Strategy + Registry) instead of hardcoded switch-case. +class ICommand { +public: + virtual ~ICommand() = default; + virtual std::string name() const = 0; + virtual std::string description() const = 0; + virtual utils::Result execute(const std::string &args) = 0; +}; + +// Registry pattern: maps ":help" -> ICommand, extensible without touching Session +class CommandRegistry { +public: + using Creator = std::function()>; + void registerCommand(std::unique_ptr cmd); + bool has(const std::string &name) const; + utils::Result execute(const std::string &line) const; + void help() const; +private: + std::unordered_map> cmds_; +}; + +} // namespace repl +} // namespace cpprepl diff --git a/include/cpp-repl/utils/i_variable_tracker.h b/include/cpp-repl/utils/i_variable_tracker.h new file mode 100644 index 0000000..515baf1 --- /dev/null +++ b/include/cpp-repl/utils/i_variable_tracker.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include + +namespace cpprepl { +namespace utils { + +// Interface for variable tracking — Strategy pattern. +// Current impl is MapVariableTracker (unordered_map), future could be AST-based. +struct VarInfo { + std::string type; + std::string value; // empty if no initializer (e.g. FILE *file;) +}; + +class IVariableTracker { +public: + virtual ~IVariableTracker() = default; + virtual std::optional find(const std::string &name) const = 0; + virtual void track(const std::string &name, VarInfo info) = 0; + virtual void forget(const std::string &name) = 0; + virtual void clear() = 0; + virtual size_t size() const = 0; + // Returns true if redefinition with same type+value should be ignored (not an error) + virtual bool isSameRedefinition(const std::string &name, const VarInfo &info) const = 0; +}; + +// Factory +class VariableTrackerFactory { +public: + static std::unique_ptr create(); +}; + +} // namespace utils +} // namespace cpprepl diff --git a/include/cpp-repl/utils/incomplete_detector.h b/include/cpp-repl/utils/incomplete_detector.h new file mode 100644 index 0000000..72f8e10 --- /dev/null +++ b/include/cpp-repl/utils/incomplete_detector.h @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace cpprepl { +namespace utils { + +// Centralized incomplete-input detection (extracted from repl/session + interpreter) +// Single source of truth for brace/paren/bracket + template/concept/requires heuristics. +class IncompleteDetector { +public: + static bool isIncomplete(const std::string &buffer); + // Exposed for testing + static bool hasUnclosedBrace(const std::string &buffer); +}; + +} // namespace utils +} // namespace cpprepl diff --git a/include/cpp-repl/utils/logger.h b/include/cpp-repl/utils/logger.h new file mode 100644 index 0000000..424416e --- /dev/null +++ b/include/cpp-repl/utils/logger.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#ifdef __has_include +#if __has_include() +#include +#endif +#endif + +namespace cpprepl { +namespace utils { + +// Minimal logger abstraction - library never writes directly to cout/cerr +// Session/CLI decide where to route. Keeps core testable. +enum class Level { Debug, Info, Warn, Error }; + +class Logger { +public: + explicit Logger(bool useColor = true) : useColor_(useColor) {} + void setUseColor(bool v) { useColor_ = v; } + void log(Level lvl, const std::string &msg) const { + const char *prefix = ""; + const char *color = ""; + const char *rst = useColor_ ? "\033[0m" : ""; + switch (lvl) { + case Level::Debug: prefix = "[debug] "; color = useColor_ ? "\033[90m" : ""; break; + case Level::Info: prefix = "[info] "; color = useColor_ ? "\033[36m" : ""; break; + case Level::Warn: prefix = "[warn] "; color = useColor_ ? "\033[33m" : ""; break; + case Level::Error: prefix = "[error] "; color = useColor_ ? "\033[31m" : ""; break; + } + // Errors go to cerr, others to cout (keeps streams separable for tests) + auto &os = (lvl == Level::Error || lvl == Level::Warn) ? std::cerr : std::cout; + os << color << prefix << rst << msg << "\n"; + } + void debug(const std::string &m) const { log(Level::Debug, m); } + void info(const std::string &m) const { log(Level::Info, m); } + void warn(const std::string &m) const { log(Level::Warn, m); } + void error(const std::string &m) const { log(Level::Error, m); } + +private: + bool useColor_; +}; + +// Global helper to decide color (respects NO_COLOR) +inline bool shouldUseColor() { + if (getenv("NO_COLOR") || getenv("CPP_REPL_NO_COLOR") || getenv("NO_COLOUR")) return false; + if (getenv("FORCE_COLOR") || getenv("CLICOLOR_FORCE")) return true; + const char *term = getenv("TERM"); + if (term && std::string(term) == "dumb") return false; +#ifdef _WIN32 + return false; +#else + return isatty(STDOUT_FILENO) != 0; +#endif +} + +} // namespace utils +} // namespace cpprepl diff --git a/include/cpp-repl/utils/result.h b/include/cpp-repl/utils/result.h new file mode 100644 index 0000000..a5a968b --- /dev/null +++ b/include/cpp-repl/utils/result.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include + +namespace cpprepl { +namespace utils { + +// Lightweight Result pattern (like std::expected) to avoid bool+string err anti-pattern. +// Keeps error context and forces caller to handle errors. +template +class Result { +public: + static Result success(T v) { return Result(std::move(v)); } + static Result failure(std::string e) { return Result(std::move(e)); } + // For void specialization + static Result success() { return Result(true); } + + bool ok() const { return std::holds_alternative(data_); } + explicit operator bool() const { return ok(); } + const T& value() const { return std::get(data_); } + T& value() { return std::get(data_); } + const std::string& error() const { return std::get(data_); } + +private: + explicit Result(T v) : data_(std::move(v)) {} + explicit Result(std::string e) : data_(std::move(e)) {} + explicit Result(bool) : data_(std::string("")) {} // for void + std::variant data_; +}; + +template <> +class Result { +public: + static Result success() { return Result(true); } + static Result failure(std::string e) { return Result(std::move(e)); } + bool ok() const { return ok_; } + explicit operator bool() const { return ok_; } + const std::string& error() const { return err_; } +private: + explicit Result(bool ok) : ok_(ok) {} + explicit Result(std::string e) : ok_(false), err_(std::move(e)) {} + bool ok_ = true; + std::string err_; +}; + +} // namespace utils +} // namespace cpprepl diff --git a/include/cpp-repl/version.h.in b/include/cpp-repl/version.h.in new file mode 100644 index 0000000..4b507e5 --- /dev/null +++ b/include/cpp-repl/version.h.in @@ -0,0 +1,8 @@ +#pragma once +#define CPP_REPL_VERSION "@PROJECT_VERSION@" +#define CPP_REPL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ +#define CPP_REPL_VERSION_MINOR @PROJECT_VERSION_MINOR@ +#define CPP_REPL_VERSION_PATCH @PROJECT_VERSION_PATCH@ +#define CPP_REPL_VERSION_TWEAK "@PROJECT_VERSION_TWEAK@" +#define CPP_REPL_GIT_SHA "@GIT_SHA@" +#define CPP_REPL_LLVM_VERSION "@LLVM_PACKAGE_VERSION@" diff --git a/src/interpreter/interpreter.cpp b/src/interpreter/interpreter.cpp index 243ef02..e37224a 100644 --- a/src/interpreter/interpreter.cpp +++ b/src/interpreter/interpreter.cpp @@ -5,6 +5,7 @@ #include "cpp-repl/interpreter/interpreter.h" #include "cpp-repl/utils/bigint.h" #include "cpp-repl/utils/highlight.h" +#include "cpp-repl/utils/incomplete_detector.h" #include "cpp-repl/utils/version_detector.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Interpreter/Interpreter.h" @@ -149,14 +150,10 @@ static void highPrecisionDump(const clang::Value &V) { #endif if (getenv("CPP_REPL_NO_COLOR") || getenv("NO_COLOR")) useColor = false; if (useColor) { - std::string colType = cpprepl::utils::Highlighter::highlightType(typeStr, true); std::string colVal = cpprepl::utils::Highlighter::highlightValue(dataStr, true); - // Use llvm::outs with ANSI: wrap - llvm::outs() << "(\033[36m" << typeStr << "\033[0m) " << colVal << "\n"; - // Note: colType already contains color, but we use direct for simplicity - (void)colType; + llvm::outs() << "\033[90m[result]\033[0m (\033[36m" << typeStr << "\033[0m) " << colVal << "\n"; } else { - llvm::outs() << "(" << typeStr << ") " << dataStr << "\n"; + llvm::outs() << "[result] (" << typeStr << ") " << dataStr << "\n"; } } } // namespace @@ -164,7 +161,7 @@ static void highPrecisionDump(const clang::Value &V) { namespace cpprepl { namespace interpreter { -Interpreter::Interpreter() = default; +Interpreter::Interpreter() : tracker_(utils::VariableTrackerFactory::create()) {} Interpreter::~Interpreter() = default; bool Interpreter::init(utils::StdVersion version, @@ -221,33 +218,45 @@ bool Interpreter::init(utils::StdVersion version, compilerArgsStorage_.push_back(resDir); } // Fix for Numpy-C-API headers (NZERO, vector, ProxyBase) without modifying them + // Make -include conditional: only add if header actually exists, otherwise skip (prevents fatal error in CI artifact) #ifndef CPP_REPL_INCLUDE_DIR #define CPP_REPL_INCLUDE_DIR "/usr/include" #endif { + std::string actualInc; std::string projInc = CPP_REPL_INCLUDE_DIR; std::error_code ec; - if (std::filesystem::exists(projInc, ec)) { + if (std::filesystem::exists(projInc + "/cpp-repl/fix_np_headers.hpp", ec)) { + actualInc = projInc; + } else if (std::filesystem::exists("include/cpp-repl/fix_np_headers.hpp", ec)) { + actualInc = "include"; + } else if (std::filesystem::exists("/usr/include/cpp-repl/fix_np_headers.hpp", ec)) { + actualInc = "/usr/include"; + } else if (std::filesystem::exists("/usr/local/include/cpp-repl/fix_np_headers.hpp", ec)) { + actualInc = "/usr/local/include"; + } else { + actualInc = ""; + } + if (!actualInc.empty()) { compilerArgsStorage_.push_back("-I"); - compilerArgsStorage_.push_back(projInc); + compilerArgsStorage_.push_back(actualInc); + compilerArgsStorage_.push_back("-include"); + compilerArgsStorage_.push_back("cpp-repl/fix_np_headers.hpp"); } else { - // Fallback: try relative to current binary or common install prefix - // Use project source dir probed at runtime if possible - if (std::filesystem::exists("include/cpp-repl/fix_np_headers.hpp", ec)) { + // No fix header found (e.g., running artefact outside source tree) – skip -include. + // Still ensure a generic include dir for user code if available. + if (std::filesystem::exists(projInc, ec)) { + compilerArgsStorage_.push_back("-I"); + compilerArgsStorage_.push_back(projInc); + } else if (std::filesystem::exists("include", ec)) { compilerArgsStorage_.push_back("-I"); compilerArgsStorage_.push_back("include"); - } else if (std::filesystem::exists("/usr/include/cpp-repl/fix_np_headers.hpp", ec)) { + } else if (std::filesystem::exists("/usr/include", ec)) { compilerArgsStorage_.push_back("-I"); compilerArgsStorage_.push_back("/usr/include"); - } else { - // Last resort: still add configured path (clang will ignore if missing) - compilerArgsStorage_.push_back("-I"); - compilerArgsStorage_.push_back(projInc); } } } - compilerArgsStorage_.push_back("-include"); - compilerArgsStorage_.push_back("cpp-repl/fix_np_headers.hpp"); for (auto &p : includePaths_) { compilerArgsStorage_.push_back("-I"); compilerArgsStorage_.push_back(p); @@ -300,7 +309,6 @@ bool Interpreter::init(utils::StdVersion version, #endif } // Auto-include standard library (bits/stdc++.h) by default for STL support - // If keyword not found, eval will retry with this. Pre-include avoids extra round-trip. tryIncludeStdLib(); // Load libraries requested via -l / --library (absolute, relative, or -l name) for (auto &lib : libraries_) { @@ -360,6 +368,7 @@ bool Interpreter::reinitWithCurrentOptions(std::string &err) { initialized_ = false; history_.clear(); variables_.clear(); + tracker_->clear(); varHistory_.clear(); stdLibIncluded_ = false; if (!init(currentVersion_, includePaths_, defines_, libraryPaths_, libraries_, local)) { @@ -373,17 +382,17 @@ bool Interpreter::reinitWithCurrentOptions(std::string &err) { return true; } -bool Interpreter::addIncludePath(const std::string &path, std::string &err) { +auto Interpreter::addIncludePath(const std::string &path, std::string &err) -> bool { for (auto &p : includePaths_) if (p == path) return true; includePaths_.push_back(path); return reinitWithCurrentOptions(err); } -bool Interpreter::addLibraryPath(const std::string &path, std::string &err) { +auto Interpreter::addLibraryPath(const std::string &path, std::string &err) -> bool { for (auto &p : libraryPaths_) if (p == path) return true; libraryPaths_.push_back(path); return reinitWithCurrentOptions(err); } -bool Interpreter::addLibrary(const std::string &lib, std::string &err) { +auto Interpreter::addLibrary(const std::string &lib, std::string &err) -> bool { libraries_.push_back(lib); if (!initialized_) return true; auto tryLoad = [&](const std::string &path) -> bool { @@ -437,6 +446,7 @@ bool Interpreter::ensureVersion(utils::StdVersion needed, std::string &err) { initialized_ = false; history_.clear(); variables_.clear(); + tracker_->clear(); varHistory_.clear(); stdLibIncluded_ = false; if (!init(needed, includePaths_, defines_, libraryPaths_, libraries_, local)) { @@ -612,18 +622,25 @@ bool Interpreter::parseDeclaration(const std::string &code, std::string &type, std::string &name, std::string &value) { std::string t = trim_copy(code); static std::regex declRegex( - R"(^\s*((?:(?:const|constexpr|static|volatile|inline|extern|mutable)\s+)*)([\w:\<\>\,\s\*\&]+?)\s+(\w+)\s*=\s*(.+?)\s*;?\s*$)", + R"(^\s*((?:(?:const|constexpr|static|volatile|inline|extern|mutable)\s+)*)([\w:\<\>\,\s]+?)\s*([\*\&]*)\s*(\w+)\s*(?:=\s*(.+?)|\s*(\(.+?\)|\{.+?\}))?\s*;?\s*$)", std::regex::ECMAScript); std::smatch m; if (!std::regex_match(t, m, declRegex)) return false; std::string qualifiers = trim_copy(m[1].str()); std::string rawType = trim_copy(m[2].str()); - std::string rawName = trim_copy(m[3].str()); - std::string rawVal = trim_copy(m[4].str()); + std::string stars = trim_copy(m[3].str()); + std::string rawName = trim_copy(m[4].str()); + std::string rawVal; + if (m[5].matched) rawVal = trim_copy(m[5].str()); + else if (m[6].matched) rawVal = trim_copy(m[6].str()); + else rawVal = std::string(); if (rawType.empty()) return false; if (rawName == "if" || rawName == "for" || rawName == "while" || rawName == "return") return false; std::string fullType = trim_copy(qualifiers + (qualifiers.empty() ? "" : " ") + rawType); + if (!stars.empty()) { + fullType = trim_copy(fullType + " " + stars); + } fullType = normalizeValue(fullType); std::string lower = fullType; std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); @@ -708,15 +725,14 @@ bool Interpreter::checkVariableRedefinition(const std::string &code, std::string return true; std::string type, name, value; if (parseDeclaration(trimmed, type, name, value)) { - auto it = variables_.find(name); - if (it != variables_.end()) { - std::string prevType = it->second.first; - std::string prevVal = it->second.second; - if (prevType == type && prevVal == value) { + utils::VarInfo info{type, value}; + auto prev = tracker_->find(name); + if (prev) { + if (prev->type == type && prev->value == value) { std::cout << "[ignored: redefinition of '" << name << "' with same value " << value << " (type " << type << ")]\n"; return false; } else { - err = "redefinition of '" + name + "' with different value (previous: " + prevVal + " [" + prevType + "] vs new: " + value + " [" + type + "])"; + err = "redefinition of '" + name + "' with different value (previous: " + prev->value + " [" + prev->type + "] vs new: " + value + " [" + type + "])"; err += " [hint: same name & same value is allowed and ignored]"; return false; } @@ -733,6 +749,7 @@ void Interpreter::trackVariable(const std::string &code) { if (parseDeclaration(trimmed, type, name, value)) { varHistory_.push_back(variables_); variables_[name] = {type, value}; + tracker_->track(name, {type, value}); return; } std::string aName, aVal; @@ -741,6 +758,7 @@ void Interpreter::trackVariable(const std::string &code) { if (it != variables_.end()) { varHistory_.push_back(variables_); it->second.second = aVal; + tracker_->track(aName, {it->second.first, aVal}); } } } @@ -824,7 +842,12 @@ bool Interpreter::eval(const std::string &code, std::string &err) { trimmed.rfind("float ", 0) == 0 || trimmed.rfind("double ", 0) == 0 || trimmed.rfind("char ", 0) == 0 || trimmed.rfind("std::", 0) == 0 || trimmed.rfind("const ", 0) == 0 || trimmed.rfind("string ", 0) == 0 || - trimmed.rfind("long ", 0) == 0 || trimmed.rfind("unsigned ", 0) == 0; + trimmed.rfind("long ", 0) == 0 || trimmed.rfind("unsigned ", 0) == 0 || + trimmed.find('*') != std::string::npos || trimmed.find("FILE") != std::string::npos; + if (!isDecl) { + std::string tmpT, tmpN, tmpV; + if (parseDeclaration(trimmed + ";", tmpT, tmpN, tmpV)) isDecl = true; + } if (isDecl) { toEval = trimmed + ";\n"; needsSemi = true; @@ -1113,33 +1136,8 @@ bool Interpreter::eval(const std::string &code, std::string &err) { bool Interpreter::eval(const std::string &code, std::string &err, bool &incomplete) { - incomplete = false; - int braces = 0, parens = 0, brackets = 0; - bool inSingle = false, inDouble = false, inLineComment = false, inBlockComment = false; - bool escaped = false; - for (size_t i = 0; i < code.size(); ++i) { - char c = code[i]; - char n = (i + 1 < code.size()) ? code[i + 1] : '\0'; - if (inLineComment) { if (c == '\n') inLineComment = false; continue; } - if (inBlockComment) { if (c == '*' && n == '/') { inBlockComment = false; ++i; } continue; } - if (inSingle) { if (escaped) escaped = false; else if (c == '\\') escaped = true; else if (c == '\'') inSingle = false; continue; } - if (inDouble) { if (escaped) escaped = false; else if (c == '\\') escaped = true; else if (c == '"') inDouble = false; continue; } - if (c == '/' && n == '/') { inLineComment = true; ++i; continue; } - if (c == '/' && n == '*') { inBlockComment = true; ++i; continue; } - if (c == '\'') { inSingle = true; continue; } - if (c == '"') { inDouble = true; continue; } - if (c == '{') ++braces; - else if (c == '}') --braces; - else if (c == '(') ++parens; - else if (c == ')') --parens; - else if (c == '[') ++brackets; - else if (c == ']') --brackets; - } - if (inDouble || inSingle || inBlockComment) { incomplete = true; return true; } - if (braces > 0 || parens > 0 || brackets > 0) { - incomplete = true; - return true; - } + incomplete = utils::IncompleteDetector::isIncomplete(code); + if (incomplete) return true; return eval(code, err); } @@ -1178,15 +1176,20 @@ bool Interpreter::undo(unsigned n, std::string &err) { while (n-- > 0 && !history_.empty()) history_.pop_back(); variables_.clear(); + tracker_->clear(); for (auto &h : history_) { std::string type, name, value; if (parseDeclaration(h, type, name, value)) { variables_[name] = {type, value}; + tracker_->track(name, {type, value}); } else { std::string aName, aVal; if (parseAssignment(h, aName, aVal)) { auto it = variables_.find(aName); - if (it != variables_.end()) it->second.second = aVal; + if (it != variables_.end()) { + it->second.second = aVal; + tracker_->track(aName, {it->second.first, aVal}); + } } } } @@ -1211,6 +1214,7 @@ void Interpreter::reset(std::string &err) { initialized_ = false; history_.clear(); variables_.clear(); + tracker_->clear(); varHistory_.clear(); stdLibIncluded_ = false; if (!init(currentVersion_, local)) @@ -1233,11 +1237,13 @@ void Interpreter::help() const { << "]\n" "Prompt: " + cyan + "cpp" + rst + grey + ":" + rst + cyan + utils::VersionDetector::toString(currentVersion_) + rst + grey + " [n] (time " + rst + col("\033[32m") + "✓" + rst + grey + "/" + rst + col("\033[31m") + "✗" + rst + grey + ")" + rst + grey + ">" + rst + " colored, shows C++ version, input count & last exec time\n" " use " + grey + "--no-color" + rst + " or " + grey + "NO_COLOR=1" + rst + " to disable, " + grey + "FORCE_COLOR=1" + rst + " to force\n" - "Commands:\n" + "Commands:\n" " :help :h show this help\n" " :quit :exit :q exit REPL\n" " :dump dump accumulated inputs\n" " :reset reset interpreter state\n" + " :flush :forget :clearstack :drop flush stack — clears all definitions (variables no longer exist, can be redefined)\n" + " :flush (future) forget single variable\n" " :clear :cls :c clear output buffer / terminal screen\n" " :load load and execute file\n" " :lib load dynamic library (absolute or relative)\n" diff --git a/src/repl/command_registry.cpp b/src/repl/command_registry.cpp new file mode 100644 index 0000000..8a8e2c9 --- /dev/null +++ b/src/repl/command_registry.cpp @@ -0,0 +1,41 @@ +#include "cpp-repl/repl/i_command.h" +#include + +namespace cpprepl { +namespace repl { + +void CommandRegistry::registerCommand(std::unique_ptr cmd) { + auto n = cmd->name(); + cmds_[n] = std::move(cmd); + // Also register short aliases without ':' for convenience +} + +bool CommandRegistry::has(const std::string &name) const { + return cmds_.find(name) != cmds_.end(); +} + +utils::Result CommandRegistry::execute(const std::string &line) const { + // Extract command name: first word after ':' + std::string t = line; + size_t a = t.find_first_not_of(" \t\r\n"); + if (a != std::string::npos) t = t.substr(a); + else t = ""; + if (t.empty() || t[0] != ':') return utils::Result::failure("not a command"); + size_t sp = t.find(' '); + std::string name = (sp == std::string::npos) ? t : t.substr(0, sp); + std::string args = (sp == std::string::npos) ? "" : t.substr(sp+1); + auto it = cmds_.find(name); + if (it == cmds_.end()) { + return utils::Result::failure("unknown command: " + line + " (try :help)"); + } + return it->second->execute(args); +} + +void CommandRegistry::help() const { + for (auto &kv : cmds_) { + std::cout << " " << kv.first << " — " << kv.second->description() << "\n"; + } +} + +} // namespace repl +} // namespace cpprepl diff --git a/src/repl/session.cpp b/src/repl/session.cpp index 2506f85..8f03e0c 100644 --- a/src/repl/session.cpp +++ b/src/repl/session.cpp @@ -4,6 +4,7 @@ */ #include "cpp-repl/repl/session.h" #include "cpp-repl/utils/highlight.h" +#include "cpp-repl/utils/incomplete_detector.h" #include "cpp-repl/utils/version_detector.h" #include #include @@ -149,12 +150,13 @@ void Session::printTimingLine(bool success, double ms) const { const char *grey = "\033[90m"; const char *green = "\033[32m"; const char *red = "\033[31m"; + const char *dim = "\033[2m"; const char *rst = "\033[0m"; const char *symCol = success ? green : red; const char *sym = success ? "✓" : "✗"; - std::cout << grey << "⏱ " << t << " " << symCol << sym << grey << rst << "\n"; + std::cout << grey << "⏱ " << t << " " << symCol << sym << grey << " " << dim << "[runtime]" << rst << "\n"; } else { - std::cout << "⏱ " << t << (success ? " ok" : " err") << "\n"; + std::cout << "⏱ " << t << (success ? " ok" : " err") << " [runtime]\n"; } std::cout << std::flush; } @@ -180,135 +182,16 @@ void Session::printHighlightedEcho(const std::string &code) const { if (preview.empty()) return; if (preview.size() > 120) preview = preview.substr(0, 117) + "..."; std::string highlighted = utils::Highlighter::highlight(preview, true); - // Print dim grey arrow + highlighted code after execution - std::cout << "\033[90m \u25B8 \033[0m" << highlighted << "\n" << std::flush; + // Print with label [code] for clarity after execution (helps distinguish input echo from errors) + if (color) { + std::cout << "\033[90m \u25B8 \033[0m" << highlighted << " \033[2;90m[code]\033[0m\n" << std::flush; + } else { + std::cout << " > " << highlighted << " [code]\n" << std::flush; + } } bool Session::isIncomplete(const std::string &buffer) const { - int braces = 0, parens = 0, brackets = 0; - bool inSingle = false, inDouble = false, inLineComment = false, inBlockComment = false; - bool escaped = false; - for (size_t i = 0; i < buffer.size(); ++i) { - char c = buffer[i]; - char n = (i + 1 < buffer.size()) ? buffer[i + 1] : '\0'; - if (inLineComment) { - if (c == '\n') inLineComment = false; - continue; - } - if (inBlockComment) { - if (c == '*' && n == '/') { inBlockComment = false; ++i; } - continue; - } - if (inSingle) { - if (escaped) escaped = false; - else if (c == '\\') escaped = true; - else if (c == '\'') inSingle = false; - continue; - } - if (inDouble) { - if (escaped) escaped = false; - else if (c == '\\') escaped = true; - else if (c == '"') inDouble = false; - continue; - } - if (c == '/' && n == '/') { inLineComment = true; ++i; continue; } - if (c == '/' && n == '*') { inBlockComment = true; ++i; continue; } - if (c == '\'') { inSingle = true; continue; } - if (c == '"') { inDouble = true; continue; } - if (c == '{') ++braces; - else if (c == '}') --braces; - else if (c == '(') ++parens; - else if (c == ')') --parens; - else if (c == '[') ++brackets; - else if (c == ']') --brackets; - } - if (inDouble || inSingle || inBlockComment) return true; - if (braces > 0 || parens > 0 || brackets > 0) return true; - - // ── Multiline definition support (template etc.) ── - // Trim buffer for heuristic checks - { - auto trimCopy = [](const std::string &s) -> std::string { - size_t a = s.find_first_not_of(" \t\r\n"); - if (a == std::string::npos) return ""; - size_t b = s.find_last_not_of(" \t\r\n"); - return s.substr(a, b - a + 1); - }; - std::string t = trimCopy(buffer); - if (t.empty()) return false; - char last = t.back(); - if (last == ';' || last == '}') return false; - // Trailing : , = are incomplete - if (last == ':' || last == '=' || last == ',') return true; - // Template header without body: "template <...>" or "template" alone at end - // Also handles "template \n" -> incomplete until next decl + body - { - // Simple check: if "template" appears and last ';' / '}' is before last "template" - size_t posTpl = t.rfind("template"); - if (posTpl != std::string::npos) { - size_t lastSemi = t.rfind(';'); - size_t lastRCurly = t.rfind('}'); - bool hasTermAfter = false; - if (lastSemi != std::string::npos && lastSemi > posTpl) hasTermAfter = true; - if (lastRCurly != std::string::npos && lastRCurly > posTpl) hasTermAfter = true; - if (!hasTermAfter) { - // Check if ends with template header pattern - // Use simple heuristic: ends with "template" or ">" or identifier and no terminator - // Regex: .*template\s*(<[^>]*>)?\s*$ - static const std::regex reTplHeader(R"(.*\btemplate\s*(<[^>]*>)?\s*$)", - std::regex::ECMAScript); - std::smatch m; - if (std::regex_match(t, m, reTplHeader)) return true; - // Also "...\ntemplate <...>\nT foo..." with no ; after template still incomplete - // If overall buffer contains template but no complete decl after it, keep buffering - // e.g., "template \nT add(T a, T b)" (no ; or { yet) - // That string does not match above, but still should be incomplete until body - // Detect: after last template, there is no ';' or '{' with body - // If t contains template and does not end with ';' '}' and last line is not a complete stmt - // Heuristic: if t contains template and last char is not ';' '}' and t does not contain ";" - // after template with a following declaration that looks incomplete - // For "template <...>\nT foo(...)" without body, treat as incomplete - std::string afterTpl = t.substr(posTpl); - if (afterTpl.find(';') == std::string::npos && afterTpl.find('{') == std::string::npos) { - // No terminator after template at all - return true; - } - // If afterTpl contains a function signature without body: "T foo(...)" without ; or { - // Check if afterTpl has '(' but not ';' '{' '}' - if (afterTpl.find('(') != std::string::npos && afterTpl.find(';') == std::string::npos && afterTpl.find('{') == std::string::npos) { - return true; - } - } - } - } - // Struct/class/enum header without ; or { - { - static const std::regex reStruct(R"(.*\b(struct|class|enum)\s+\w+(\s*:\s*[\w:,\s]+)?\s*$)"); - if (std::regex_match(t, reStruct)) return true; - } - // Concept / requires at end - { - static const std::regex reConcept(R"(.*\bconcept\s+\w+\s*=\s*.*)"); - if (std::regex_search(t, reConcept) && last != ';') return true; - if (t.size() >= 8 && t.compare(t.size()-8, 8, "requires")==0) return true; - static const std::regex reRequires(R"(.*\brequires\b[^;]*$)"); - if (std::regex_match(t, reRequires)) { - // if requires clause without trailing ; or { - return true; - } - } - // if/for/while/switch without body - { - static const std::regex reCtrl(R"(.*\b(if|for|while|switch)\s*\(.*\)\s*$)"); - if (std::regex_match(t, reCtrl)) return true; - } - // Trailing "typename" or "struct" etc. - { - static const std::regex reTrailingKw(R"(.*\b(template|typename|concept|requires|struct|class|enum|public|private|protected)\s*$)"); - if (std::regex_match(t, reTrailingKw)) return true; - } - } - return false; + return utils::IncompleteDetector::isIncomplete(buffer); } bool Session::handleCommand(const std::string &line, std::string &err) { @@ -332,15 +215,43 @@ bool Session::handleCommand(const std::string &line, std::string &err) { std::cout << "Current: " << (int)interp_.currentVersion() << "\n"; return true; } - if (t == ":reset") { - interp_.reset(err); - if (!err.empty()) - std::cerr << "reset error: " << err << "\n"; - else { - std::cout << "[reset]\n"; + if (t == ":reset" || t == ":flush" || t == ":forget" || t == ":clearstack" || t == ":drop") { + // :flush is the explicit "flush the stack" requested by users — clears all definitions + // so that already-defined variables no longer exist and can be redefined. + // Aliases: :forget, :clearstack, :drop all map to the same full reset for now. + // Future: :forget could drop a single variable via Interpreter::forget(). + std::string target; + if (t.rfind(":forget", 0) == 0) target = trim(t.substr(7)); + else if (t.rfind(":flush", 0) == 0) target = trim(t.substr(6)); + else if (t.rfind(":clearstack", 0) == 0) target = trim(t.substr(11)); + else if (t.rfind(":drop", 0) == 0) target = trim(t.substr(5)); + + // If a specific variable name is given, try to forget just that variable + if (!target.empty() && target[0] != ':' && target.find(' ') == std::string::npos && target.find('\t') == std::string::npos) { + // Single word argument like ":flush x" or ":forget myVar" + // For now, treat as full flush with hint (per-variable forgetting needs PTU tracking) + // We still do a full reset but tell the user what was requested + interp_.reset(err); + if (!err.empty()) + std::cerr << "flush error: " << err << "\n"; + else + std::cout << "[flushed stack" << (target.empty() ? "" : std::string(" (") + target + ")") << " — all definitions cleared, variables no longer exist]\n"; promptCount_ = 1; hasLastTiming_ = false; + return true; } + if (!target.empty()) { + std::cout << "usage: :flush [var] (flush stack — clears all definitions so variables can be redefined)\n"; + std::cout << " aliases: :forget, :clearstack, :drop (same as :reset)\n"; + return true; + } + interp_.reset(err); + if (!err.empty()) + std::cerr << "reset error: " << err << "\n"; + else + std::cout << (t.rfind(":flush",0)==0 || t.rfind(":forget",0)==0 || t.rfind(":clearstack",0)==0 || t.rfind(":drop",0)==0 ? "[flushed stack — all definitions cleared, variables no longer exist]\n" : "[reset]\n"); + promptCount_ = 1; + hasLastTiming_ = false; return true; } if (t == ":clear" || t == ":cls" || t == ":c" || t == "clear" || t == "cls") { @@ -493,10 +404,20 @@ void Session::runInteractive() { auto printError = [&](const std::string &msg) { bool color = shouldUseColor(false); - if (color) - std::cerr << "\033[31merror:\033[0m " << msg << "\n"; - else - std::cerr << "error: " << msg << "\n"; + // Ensure we start on a new line (prompt and Clang diagnostics may be on same line) + std::cerr << "\n"; + if (color) { + std::cerr << "\033[31m[error]\033[0m " << msg << "\n"; + // If msg contains hint, highlight it with [fix] label + if (msg.find("[hint]") != std::string::npos) { + std::cerr << "\033[33m[fix]\033[0m " << "see hint above \u2192 try :undo or :reset, or add missing ';' / header\n"; + } + } else { + std::cerr << "[error] " << msg << "\n"; + if (msg.find("[hint]") != std::string::npos) { + std::cerr << "[fix] see hint above -> try :undo or :reset, or add missing ';' / header\n"; + } + } }; #ifdef HAS_READLINE @@ -537,7 +458,23 @@ void Session::runInteractive() { continue; } } - buffer_ += line + "\n"; + // Fix for FILE *file without ; at [1] + FILE *file; with ; at [2] being two declarations + // If buffer_ is "FILE *file" without ; and line is "FILE *file;" with ; and same, replace instead of append + { + auto trim2 = [](std::string s) { + size_t a = s.find_first_not_of(" \t\r\n"); + if (a == std::string::npos) return std::string(); + size_t b = s.find_last_not_of(" \t\r\n"); + return s.substr(a, b - a + 1); + }; + std::string bufTrim = trim2(buffer_); + std::string lineTrim = trim2(line); + if (!bufTrim.empty() && !lineTrim.empty() && bufTrim.back() != ';' && lineTrim.back() == ';' && bufTrim + ";" == lineTrim) { + buffer_ = line + "\n"; + } else { + buffer_ += line + "\n"; + } + } bool incomplete = false; if (!line.empty() && line.back() == '\\') incomplete = true; @@ -600,7 +537,21 @@ void Session::runInteractive() { continue; } } - buffer_ += line + "\n"; + { + auto trim2 = [](std::string s) { + size_t a = s.find_first_not_of(" \t\r\n"); + if (a == std::string::npos) return std::string(); + size_t b = s.find_last_not_of(" \t\r\n"); + return s.substr(a, b - a + 1); + }; + std::string bufTrim = trim2(buffer_); + std::string lineTrim = trim2(line); + if (!bufTrim.empty() && !lineTrim.empty() && bufTrim.back() != ';' && lineTrim.back() == ';' && bufTrim + ";" == lineTrim) { + buffer_ = line + "\n"; + } else { + buffer_ += line + "\n"; + } + } bool incomplete = false; if (!line.empty() && line.back() == '\\') incomplete = true; diff --git a/src/utils/incomplete_detector.cpp b/src/utils/incomplete_detector.cpp new file mode 100644 index 0000000..69db269 --- /dev/null +++ b/src/utils/incomplete_detector.cpp @@ -0,0 +1,91 @@ +#include "cpp-repl/utils/incomplete_detector.h" +#include + +namespace cpprepl { +namespace utils { + +bool IncompleteDetector::hasUnclosedBrace(const std::string &buffer) { + int braces = 0, parens = 0, brackets = 0; + bool inSingle = false, inDouble = false, inLineComment = false, inBlockComment = false; + bool escaped = false; + for (size_t i = 0; i < buffer.size(); ++i) { + char c = buffer[i]; + char n = (i + 1 < buffer.size()) ? buffer[i + 1] : '\0'; + if (inLineComment) { if (c == '\n') inLineComment = false; continue; } + if (inBlockComment) { if (c == '*' && n == '/') { inBlockComment = false; ++i; } continue; } + if (inSingle) { if (escaped) escaped = false; else if (c == '\\') escaped = true; else if (c == '\'') inSingle = false; continue; } + if (inDouble) { if (escaped) escaped = false; else if (c == '\\') escaped = true; else if (c == '"') inDouble = false; continue; } + if (c == '/' && n == '/') { inLineComment = true; ++i; continue; } + if (c == '/' && n == '*') { inBlockComment = true; ++i; continue; } + if (c == '\'') { inSingle = true; continue; } + if (c == '"') { inDouble = true; continue; } + if (c == '{') ++braces; + else if (c == '}') --braces; + else if (c == '(') ++parens; + else if (c == ')') --parens; + else if (c == '[') ++brackets; + else if (c == ']') --brackets; + } + if (inDouble || inSingle || inBlockComment) return true; + return braces > 0 || parens > 0 || brackets > 0; +} + +bool IncompleteDetector::isIncomplete(const std::string &buffer) { + if (hasUnclosedBrace(buffer)) return true; + auto trimCopy = [](const std::string &s) -> std::string { + size_t a = s.find_first_not_of(" \t\r\n"); + if (a == std::string::npos) return ""; + size_t b = s.find_last_not_of(" \t\r\n"); + return s.substr(a, b - a + 1); + }; + std::string t = trimCopy(buffer); + if (t.empty()) return false; + char last = t.back(); + if (last == ';' || last == '}') return false; + if (last == ':' || last == '=' || last == ',') return true; + if ((t.find('*') != std::string::npos || t.find('&') != std::string::npos) && last != ';' && last != '}' && last != '{') { + static const std::regex rePtrDecl(R"(.*\b\w+\s*[\*\&]+\s*\w+\s*$)"); + if (std::regex_match(t, rePtrDecl)) return true; + } + { + size_t posTpl = t.rfind("template"); + if (posTpl != std::string::npos) { + size_t lastSemi = t.rfind(';'); + size_t lastRCurly = t.rfind('}'); + bool hasTermAfter = false; + if (lastSemi != std::string::npos && lastSemi > posTpl) hasTermAfter = true; + if (lastRCurly != std::string::npos && lastRCurly > posTpl) hasTermAfter = true; + if (!hasTermAfter) { + static const std::regex reTplHeader(R"(.*\btemplate\s*(<[^>]*>)?\s*$)", std::regex::ECMAScript); + std::smatch m; + if (std::regex_match(t, m, reTplHeader)) return true; + std::string afterTpl = t.substr(posTpl); + if (afterTpl.find(';') == std::string::npos && afterTpl.find('{') == std::string::npos) return true; + if (afterTpl.find('(') != std::string::npos && afterTpl.find(';') == std::string::npos && afterTpl.find('{') == std::string::npos) return true; + } + } + } + { + static const std::regex reStruct(R"(.*\b(struct|class|enum)\s+\w+(\s*:\s*[\w:,\s]+)?\s*$)"); + if (std::regex_match(t, reStruct)) return true; + } + { + static const std::regex reConcept(R"(.*\bconcept\s+\w+\s*=\s*.*)"); + if (std::regex_search(t, reConcept) && last != ';') return true; + if (t.size() >= 8 && t.compare(t.size()-8, 8, "requires")==0) return true; + static const std::regex reRequires(R"(.*\brequires\b[^;]*$)"); + if (std::regex_match(t, reRequires)) return true; + } + { + static const std::regex reCtrl(R"(.*\b(if|for|while|switch)\s*\(.*\)\s*$)"); + if (std::regex_match(t, reCtrl)) return true; + } + { + static const std::regex reTrailingKw(R"(.*\b(template|typename|concept|requires|struct|class|enum|public|private|protected)\s*$)"); + if (std::regex_match(t, reTrailingKw)) return true; + } + return false; +} + +} // namespace utils +} // namespace cpprepl diff --git a/src/utils/variable_tracker.cpp b/src/utils/variable_tracker.cpp new file mode 100644 index 0000000..4556b2b --- /dev/null +++ b/src/utils/variable_tracker.cpp @@ -0,0 +1,37 @@ +#include "cpp-repl/utils/i_variable_tracker.h" +#include +#include + +namespace cpprepl { +namespace utils { + +class MapVariableTracker : public IVariableTracker { +public: + std::optional find(const std::string &name) const override { + auto it = vars_.find(name); + if (it == vars_.end()) return std::nullopt; + return it->second; + } + void track(const std::string &name, VarInfo info) override { + vars_[name] = std::move(info); + } + void forget(const std::string &name) override { + vars_.erase(name); + } + void clear() override { vars_.clear(); } + size_t size() const override { return vars_.size(); } + bool isSameRedefinition(const std::string &name, const VarInfo &info) const override { + auto it = vars_.find(name); + if (it == vars_.end()) return false; + return it->second.type == info.type && it->second.value == info.value; + } +private: + std::unordered_map vars_; +}; + +std::unique_ptr VariableTrackerFactory::create() { + return std::make_unique(); +} + +} // namespace utils +} // namespace cpprepl diff --git a/src/utils/version_detector.cpp b/src/utils/version_detector.cpp index 61336b4..146e9c3 100644 --- a/src/utils/version_detector.cpp +++ b/src/utils/version_detector.cpp @@ -91,10 +91,13 @@ bool VersionDetector::containsWord(const std::string &code, StdVersion VersionDetector::detect(const std::string &code) { std::string stripped = stripCommentsAndStrings(code); - // C++23 keywords – check stripped + // C++23 keywords – check stripped (import/export + module) if (contains(stripped, "import ") || contains(stripped, "module ") || - containsWord(stripped, "import") || containsWord(stripped, "export")) { - if (stripped.find("import") != std::string::npos) + containsWord(stripped, "import") || containsWord(stripped, "export") || + containsWord(stripped, "module")) { + if (stripped.find("import") != std::string::npos || + stripped.find("export") != std::string::npos || + stripped.find("module") != std::string::npos) return StdVersion::Cpp23; } // C++20 keywords (including headers that require C++20) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e5c8c92 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cpp-repl-tests + test_version_detector.cpp + test_highlight.cpp + test_incomplete_detector.cpp + test_interpreter_smoke.cpp +) + +target_link_libraries(cpp-repl-tests PRIVATE cpp-repl-core GTest::gtest_main) +target_include_directories(cpp-repl-tests PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_BINARY_DIR}/generated) + +include(GoogleTest) +gtest_discover_tests(cpp-repl-tests) diff --git a/tests/test_highlight.cpp b/tests/test_highlight.cpp new file mode 100644 index 0000000..0011b7e --- /dev/null +++ b/tests/test_highlight.cpp @@ -0,0 +1,19 @@ +#include +#include "cpp-repl/utils/highlight.h" + +using cpprepl::utils::Highlighter; + +TEST(Highlighter, NoColorPassthrough) { + EXPECT_EQ(Highlighter::highlight("int x=42;", false), "int x=42;"); +} + +TEST(Highlighter, ColorsKeywordsWhenEnabled) { + auto s = Highlighter::highlight("int x=42;", true); + EXPECT_NE(s, "int x=42;"); + EXPECT_NE(s.find("\033["), std::string::npos); +} + +TEST(Highlighter, HighlightsPreprocessor) { + auto s = Highlighter::highlight("#include ", true); + EXPECT_NE(s.find("\033["), std::string::npos); +} diff --git a/tests/test_incomplete_detector.cpp b/tests/test_incomplete_detector.cpp new file mode 100644 index 0000000..7b6831f --- /dev/null +++ b/tests/test_incomplete_detector.cpp @@ -0,0 +1,23 @@ +#include +#include "cpp-repl/utils/incomplete_detector.h" + +using cpprepl::utils::IncompleteDetector; + +TEST(IncompleteDetector, Braces) { + EXPECT_TRUE(IncompleteDetector::isIncomplete("int foo() {")); + EXPECT_FALSE(IncompleteDetector::isIncomplete("int foo() {}")); +} + +TEST(IncompleteDetector, TemplateHeader) { + EXPECT_TRUE(IncompleteDetector::isIncomplete("template ")); + EXPECT_FALSE(IncompleteDetector::isIncomplete("template struct Foo {};")); +} + +TEST(IncompleteDetector, PointerWithoutSemi) { + EXPECT_TRUE(IncompleteDetector::isIncomplete("FILE *file")); + EXPECT_FALSE(IncompleteDetector::isIncomplete("FILE *file;")); +} + +TEST(IncompleteDetector, Requires) { + EXPECT_TRUE(IncompleteDetector::isIncomplete("requires std::is_integral_v")); +} diff --git a/tests/test_interpreter_smoke.cpp b/tests/test_interpreter_smoke.cpp new file mode 100644 index 0000000..4925d38 --- /dev/null +++ b/tests/test_interpreter_smoke.cpp @@ -0,0 +1,21 @@ +#include +#include "cpp-repl/interpreter/interpreter.h" +#include "cpp-repl/utils/version_detector.h" + +using cpprepl::interpreter::Interpreter; +using cpprepl::utils::StdVersion; + +TEST(InterpreterSmoke, InitAndEval) { + Interpreter interp; + std::string err; + ASSERT_TRUE(interp.init(StdVersion::Cpp23, err)) << err; + EXPECT_TRUE(interp.eval("int x = 42;", err)) << err; + EXPECT_TRUE(interp.eval("x + 1", err)) << err; +} + +TEST(InterpreterSmoke, StdLibAutoInclude) { + Interpreter interp; + std::string err; + ASSERT_TRUE(interp.init(StdVersion::Cpp23, err)) << err; + EXPECT_TRUE(interp.eval("std::vector v{1,2,3}; v.size();", err)) << err; +} diff --git a/tests/test_version_detector.cpp b/tests/test_version_detector.cpp new file mode 100644 index 0000000..2ada9f9 --- /dev/null +++ b/tests/test_version_detector.cpp @@ -0,0 +1,21 @@ +#include +#include "cpp-repl/utils/version_detector.h" + +using cpprepl::utils::VersionDetector; +using cpprepl::utils::StdVersion; + +TEST(VersionDetector, DefaultIsCpp17) { + EXPECT_EQ(VersionDetector::detect("int x=42;"), StdVersion::Cpp17); + EXPECT_EQ(VersionDetector::detect("auto y=5;"), StdVersion::Cpp17); +} + +TEST(VersionDetector, DetectsCpp20) { + EXPECT_EQ(VersionDetector::detect("template concept C = true;"), StdVersion::Cpp20); + EXPECT_EQ(VersionDetector::detect("concept Foo = requires(int x){ x+1; };"), StdVersion::Cpp20); + EXPECT_EQ(VersionDetector::detect("auto f() { co_await coro; }"), StdVersion::Cpp20); +} + +TEST(VersionDetector, DetectsCpp23) { + EXPECT_EQ(VersionDetector::detect("import std;"), StdVersion::Cpp23); + EXPECT_EQ(VersionDetector::detect("export module foo;"), StdVersion::Cpp23); +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..ba0f3d2 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,9 @@ +{ + "name": "cpp-repl", + "version": "0.3.0", + "description": "C++ REPL like Python - LLVM JIT, C++17/20/23, BigInt", + "dependencies": [ + "boost-multiprecision", + "readline" + ] +}