diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98782cf..265051a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,4 +105,41 @@ jobs: path: build/cpp-repl retention-days: 7 + release: + name: Automatic Release + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: cpp-repl-ubuntu-24.04-llvm22 + path: ./artifact + + - name: Prepare release archive + run: | + ls -lh ./artifact/cpp-repl + chmod +x ./artifact/cpp-repl + tar czf cpp-repl-auto-${{ github.sha }}-linux-x86_64.tar.gz -C ./artifact cpp-repl + sha256sum cpp-repl-auto-${{ github.sha }}-linux-x86_64.tar.gz | tee SHA256SUMS + cat SHA256SUMS + + - name: Create Automatic GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: auto-${{ github.sha }} + name: Automatic Release ${{ github.sha }} + target_commitish: ${{ github.sha }} + files: | + cpp-repl-*.tar.gz + SHA256SUMS + generate_release_notes: true + fail_on_unmatched_files: false + # Fast sanity job for PRs without full LLVM install (doc/lint only) could be added here diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 486c7c6..f148d8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,8 @@ name: Release on: push: + branches: + - main tags: - 'v*.*.*' workflow_dispatch: @@ -35,24 +37,32 @@ jobs: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-22 -DCMAKE_CXX_COMPILER=clang++-22 -DLLVM_DIR=/usr/lib/llvm-22/lib/cmake/llvm cmake --build build -j $(nproc) ./build/cpp-repl --version - tar czf cpp-repl-${{ github.ref_name }}-linux-x86_64.tar.gz -C build cpp-repl - sha256sum cpp-repl-${{ github.ref_name }}-linux-x86_64.tar.gz | tee SHA256SUMS + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${{ github.ref_name }}" + else + TAG="auto-${{ github.sha }}" + fi + echo "TAG=$TAG" >> $GITHUB_ENV + tar czf cpp-repl-${TAG}-linux-x86_64.tar.gz -C build cpp-repl + sha256sum cpp-repl-${TAG}-linux-x86_64.tar.gz | tee SHA256SUMS - name: Create GitHub Release - if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v2 with: + tag_name: ${{ env.TAG }} + name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('Automatic Release {0}', github.sha) }} + target_commitish: ${{ github.sha }} files: | cpp-repl-*.tar.gz SHA256SUMS generate_release_notes: true fail_on_unmatched_files: false - - name: Upload artifact (manual dispatch) - if: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload artifact (manual dispatch fallback) + if: failure() uses: actions/upload-artifact@v4 with: - name: cpp-repl-release-${{ github.ref_name || inputs.tag || 'manual' }} + name: cpp-repl-release-${{ env.TAG || github.ref_name || inputs.tag || 'manual' }} path: | cpp-repl-*.tar.gz SHA256SUMS diff --git a/.gitignore b/.gitignore index 5710deb..eb60faf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ compile_commands.json .DS_Store .cache/ ccache/ +Testing/ diff --git a/CMakeLists.txt b/CMakeLists.txt index f4a7d36..6202906 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,7 @@ add_executable(cpp-repl 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 ) diff --git a/README.md b/README.md index 00e91fc..f9691ef 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,81 @@ -# cpp-repl +# cpp-repl — C++ REPL like Python -[![CI](https://github.com/sergiorandria/cpp-repl/actions/workflows/ci.yml/badge.svg)](https://github.com/sergiorandria/cpp-repl/actions/workflows/ci.yml) -[![Format](https://github.com/sergiorandria/cpp-repl/actions/workflows/format.yml/badge.svg)](https://github.com/sergiorandria/cpp-repl/actions/workflows/format.yml) -[![CodeQL](https://github.com/sergiorandria/cpp-repl/actions/workflows/codeql.yml/badge.svg)](https://github.com/sergiorandria/cpp-repl/actions/workflows/codeql.yml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![C++17](https://img.shields.io/badge/C%2B%2B-17%2F20%2F23-blue.svg)](https://en.cppreference.com) -[![LLVM 22](https://img.shields.io/badge/LLVM-22-red.svg)](https://llvm.org) -[![last commit](https://img.shields.io/github/last-commit/sergiorandria/cpp-repl)](https://github.com/sergiorandria/cpp-repl/commits/main) +

+ cpp-repl capture +
+ Python-like incremental REPL for C++ — no main(), just raw C++ +

-Low-level C++ REPL (like Python REPL) built on LLVM VM concept. +

+ CI + Format + CodeQL + License: MIT + C++17/20/23 + LLVM 22 + last commit +

-- **VM core**: `llvm::orc::LLJIT` – in-process JIT, no optimization initially -- **Frontend**: `clang::Interpreter` (incremental parser + ORC executor) for real C++ parsing -- **REPL**: incremental state, value printing, diagnostics, `:commands` +

+ Low-level C++ REPL built on the LLVM VM.  •  LLJIT + clang::Interpreter  •  O0 correctness-first  •  auto -std  •  BigInt  •  hot -I/-L/-l +

-## Architecture (scalable) +--- + +## ⚡️ Why cpp-repl? + +| Traditional C++ | **cpp-repl** | +|---|---| +| Need `int main(){}` + recompile | **Just type raw C++** — `int x = 42; x+1` | +| `g++ file.cpp && ./a.out` | **`./build/cpp-repl file.cpp`** or `:load file.cpp` | +| Manual `-std=c++20` for concepts | **Auto-detects** `concept`/`requires`/`import` → switches to C++20/23 | +| No Python-like ints | **`cpp_int` / `bigint`** arbitrary precision out of the box | +| Absolute paths are painful | **`-I ./inc -I /abs/path -L ./lib -l mylib`** cmdline **and** interactive `:I`/`:L`/`:lib` | + +> Think `python` but for C++. Incremental state, value printing `(type) value`, diagnostics on stderr, history + undo. + +## 🎬 30-second demo + +```bash +./build/cpp-repl +cpp> int x = 42; +cpp> x + 1 +(int) 43 + +cpp> #include +cpp> std::cout << "hi" << std::endl; +hi + +cpp> auto f = [](double v){ return v*v; }; +cpp> f(234.23423948934894) +(double) 54865.278... + +cpp> cpp_int a = cpp_int("123456789012345678901234567890"); +cpp> a * a +123456789012345678901234567890 * 123456789012345678901234567890 = 152415... +``` + +Pipes & scripts work like Python: + +```bash +echo 'int x=42; x+1' | ./build/cpp-repl +./build/cpp-repl --no-interactive -e 'int x=5; x*2' -e 'x+10' +./build/cpp-repl examples/hello.cpp --no-interactive +echo 'cpp_int a=cpp_int("12345678901234567890"); a*a' | ./build/cpp-repl +``` + +## ✨ Features + +- **🧠 LLVM VM core** — `llvm::orc::LLJIT` in-process JIT, `LLVMContext`/`Module`/`IRBuilder`, pluggable `core::VM` interface (swap to `RemoteJIT`) +- **🩺 Clang frontend** — `clang::Interpreter` incremental parser + ORC executor, real C++ parsing (not a toy) +- **🔄 Incremental REPL** — `repl::Session` with `cpp>` / `...>` multiline, history, `:undo`, `:reset`, `:dump`, `:clear` +- **🎯 Auto C++ version** — `C++17` default; upgrades to `C++20` on `concept`/`requires`/`co_await`/`<=>`/`consteval`, to `C++23` on `import`/`module` +- **🔢 BigInt** — `boost::multiprecision::cpp_int` (`bigint` alias) + `mpz_int` if GMP present; strings wrapped as `cpp_int("...")` to avoid literal overflow +- **📁 Includes & libs** — absolute + relative `-I`/`-L`/`-l`/`-D` on cmdline **and** live `:I ` / `:L ` / `:lib `; survives version upgrades +- **🎨 Precise floats** — `float 9`, `double 17`, `long double 21` digits (`max_digits10`) instead of truncated `%6g`/`%8g` +- **🧪 No optimizations** — always `-O0 -g -fno-exceptions -fno-rtti`, diagnostics → stderr, `llvm::Error` handling + +## 🏗️ Architecture (scalable) ``` User Input (raw C++ like Python, no main) @@ -25,13 +86,13 @@ User Input (raw C++ like Python, no main) └──────────────┬──────────────────────────┘ ▼ ┌─────────────────────────────────────────┐ -│ utils::VersionDetector (src/utils) │ ← auto-detects C++17/20/23 via keywords +│ utils::VersionDetector (src/utils) │ ← auto-detects C++17/20/23 │ import, concept, requires, co_await │ └──────────────┬──────────────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ interpreter::Interpreter (src/interpreter) │ ← wraps clang::Interpreter + ORC JIT -│ + utils::BigIntSupport preamble │ auto -std=c++17/20/23, bigint +│ + utils::BigIntSupport preamble │ auto -std, bigint, high-prec floats └──────────────┬──────────────────────────┘ ▼ ┌─────────────────────────────────────────┐ @@ -40,18 +101,15 @@ User Input (raw C++ like Python, no main) └──────────────┬──────────────────────────┘ ▼ ┌─────────────────────────────────────────┐ -│ repl::Session (src/repl) │ ← prompts, multiline, :commands, history +│ repl::Session (src/repl) │ ← prompts, multiline, :commands └─────────────────────────────────────────┘ ``` -Scalable: each layer is an isolated module with interface, easy to swap VM (e.g. RemoteJIT), -add new frontends, or extend utils (BigInt, version). +Each layer is an isolated module with a stable interface — swap `LLJIT` for `RemoteJIT`, add a new frontend, or extend `utils` without touching the rest. -No optimizations (`O0`) – correctness first. +## 🚀 Build -## Build - -Requires LLVM 22 + Clang 22 (with `clangInterpreter`). +**Requires:** LLVM 22 + Clang 22 (with `clangInterpreter`) ```bash cmake -B build -DCMAKE_BUILD_TYPE=Debug @@ -59,7 +117,9 @@ cmake --build build -j ./build/cpp-repl ``` -## CLI (interpreter-like, no main) +> Debug is default (`O0`). Release: `-DCMAKE_BUILD_TYPE=Release`. + +## 💻 CLI (interpreter-like, no `main`) ```bash cpp-repl [options] [file ...] @@ -77,7 +137,7 @@ cpp-repl [options] [file ...] execute raw C++ file as script (like python script.py) ``` -Examples (no `int main()` required, just raw C++ like Python): +**Examples — no `int main()` needed:** ```bash ./build/cpp-repl # interactive REPL, raw code @@ -86,18 +146,22 @@ Examples (no `int main()` required, just raw C++ like Python): ./build/cpp-repl -e 'int x=5; x*2' --no-interactive # -e raw code echo 'int x=42; x+1' | ./build/cpp-repl # pipe like python echo 'cpp_int a = cpp_int("12345678901234567890"); a*a' | ./build/cpp-repl + # absolute & relative includes + libraries (cmdline) ./build/cpp-repl -I ./include -I /abs/path -L ./lib -l mylib -e '#include "myheader.h"' ./build/cpp-repl -I ./rel_include --library m --no-interactive -e 'extern "C" int mylib_add(int,int); mylib_add(2,3)' ``` -## REPL Commands (inside `cpp>`) +## ⌨️ REPL Commands + +Inside `cpp>`: ``` :help :h show help :quit :exit :q exit REPL :dump dump history + current C++ version :reset reset interpreter state +:clear :cls :c clear output buffer / terminal screen :load load raw C++ file :lib load dynamic library (abs/rel, e.g. :lib ./lib/mylib.so) :I add include search path (abs or rel, like -I) @@ -106,7 +170,9 @@ echo 'cpp_int a = cpp_int("12345678901234567890"); a*a' | ./build/cpp-repl :version show current -std version ``` -## Include & Library (absolute/relative, cmdline & interactive) +Multiline: unbalanced `{ ( [` keeps buffering with `...>` prompt. + +## 📂 Include & Library (absolute/relative, cmdline & interactive) Both **absolute** (`/usr/local/include/mylib.h`) and **relative** (`./include/mylib.h`, `../common/header.h`) are supported. @@ -125,7 +191,7 @@ cpp-repl -I ./include -L ./lib -l mylib --library gmp -D DEBUG=1 cpp-repl -I ./include -L ./lib -l mylib examples/use_mylib.cpp --no-interactive ``` -**Interactive (inside REPL):** +**Interactive:** ```cpp cpp> :I ./rel_include // relative @@ -140,10 +206,9 @@ cpp> extern "C" int mylib_add(int,int); cpp> mylib_add(7,8) // (int) 1015 ``` -Paths are stored and survive `C++` version upgrades (e.g. adding `:I` then `concept` → still keeps includes). +> Paths are stored and survive C++ version upgrades (e.g. `:I` then `concept` → still keeps includes). - -## C++ Version Support (auto-detect) +## 🧬 C++ Version Support (auto-detect) Interpreter auto-detects required `-std`: @@ -151,54 +216,52 @@ Interpreter auto-detects required `-std`: - **C++20** if code contains `concept`, `requires`, `co_await`, `co_yield`, `char8_t`, `<=>`, `consteval` - **C++23** if code contains `import`, `module`/`export` -Example: - ```cpp cpp> template concept Addable = requires(T a,T b){ a+b; }; cpp> Addable auto x = 42; // auto-detects C++20, re-inits to -std=c++20 cpp> import std; // C++23 ``` -No manual `-std` needed. +No manual `-std` needed — upgrades are replay-safe (history is re-executed on new `CompilerInstance`). -## BigInt – Very Large Numbers (GMP/mpz) +## 🔢 BigInt — Very Large Numbers (GMP/mpz) Like Python's arbitrary large ints, via `boost::multiprecision`: ```cpp cpp> cpp_int a = cpp_int("1234567890123456789012345678901234567890"); cpp> cpp_int b = cpp_int("987654321098765432109876543210"); -cpp> cpp_int c = a * b; c // prints big int +cpp> cpp_int c = a * b; c // prints big int via std::cout cpp> bigint x = cpp_int("999999999999999999999999999999"); // alias cpp> x * x ``` -`bigint` is `boost::multiprecision::cpp_int` (header-only). If GMP is installed, -also `boost::multiprecision::mpz_int` / `gmp.hpp` is available and linked via `-lgmp -lgmpxx`. +`bigint` is `boost::multiprecision::cpp_int` (header-only). If GMP is installed, also `mpz_int` / `gmp.hpp` is available and linked via `-lgmp -lgmpxx`. Large literals are auto-wrapped as `cpp_int("...")` to avoid `integer literal is too large`. Demo: `examples/bigint.cpp` - -## Examples +## 📚 Examples See `examples/`: -- `hello.cpp` – basic I/O and value printing -- `functions.cpp` – incremental function definitions (persistence) -- `class.cpp` – structs, classes, templates +- `hello.cpp` — basic I/O and value printing +- `functions.cpp` — incremental function definitions (persistence) +- `class.cpp` — structs, classes, templates +- `bigint.cpp` — big integers +- `version_concept.cpp` — C++20 concepts Load with `:load examples/hello.cpp` or `./build/cpp-repl examples/class.cpp` -## Low-level VM detail +## 🔬 Low-level VM detail -- `include/cpp-repl/core/vm.h` + `src/core/vm.cpp` – raw `llvm::orc::LLJIT` wrapper via `core::VM` interface (pluggable, scalable). Manually builds LLVM IR (`LLVMContext`, `Module`, `IRBuilder`) and JIT-executes without Clang. -- `include/cpp-repl/interpreter/interpreter.h` – `interpreter::Interpreter` wraps `clang::Interpreter` (incremental) + `VersionDetector` + `BigIntSupport` preamble. Handles raw code without `main`. +- `include/cpp-repl/core/vm.h` + `src/core/vm.cpp` — raw `llvm::orc::LLJIT` wrapper via `core::VM` interface (pluggable). Manually builds LLVM IR (`LLVMContext`, `Module`, `IRBuilder`) and JIT-executes without Clang. +- `include/cpp-repl/interpreter/interpreter.h` — `interpreter::Interpreter` wraps `clang::Interpreter` + `VersionDetector` + `BigIntSupport` preamble. Handles raw code without `main`. - Legacy `src/vm.h` / `src/repl.h` kept as wrappers for backward compat. -No optimizations: all builds are `-O0 -g -fno-exceptions -fno-rtti` to stay close to the VM. +All builds are `-O0 -g -fno-exceptions -fno-rtti` to stay close to the VM. -## Project Structure (scalable) +## 🗂️ Project Structure (scalable) ``` include/cpp-repl/ @@ -209,24 +272,28 @@ include/cpp-repl/ cli/cli.h src/ core/vm.cpp - interpreter/interpreter.cpp + interpreter/interpreter.cpp # high-precision float printing (9/17/21 digits) utils/version_detector.cpp, bigint.cpp repl/session.cpp cli/cli.cpp - main.cpp (scalable, delegates to modules) + main.cpp (delegates to modules) examples/ hello.cpp, functions.cpp, class.cpp, bigint.cpp, version_concept.cpp ``` -## CI & Quality +## ✅ CI & Quality -- **CI** (`ci.yml`): Ubuntu 24.04, LLVM 22, `Debug` + `Release`, smoke tests (`-e`, pipe, examples, BigInt, C++20 concepts) +- **CI** (`ci.yml`): Ubuntu 24.04, LLVM 22, `Debug` + `Release`, smoke tests (`-e`, pipe, examples, BigInt, C++20 concepts, float precision) - **Format** (`format.yml`): `clang-format-22` + whitespace + `cppcheck` - **CodeQL** (`codeql.yml`): weekly security analysis (C++) - **Release** (`release.yml`): builds tarball on `v*` tags via `softprops/action-gh-release` Enable branch protection on `main` to require `Build (ubuntu-24.04, Debug, LLVM 22)` before merge. -## License +## 🤝 Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). PRs welcome — run `clang-format` and `cmake --build build -j` before pushing. + +## 📄 License MIT — see [LICENSE](LICENSE) diff --git a/capture.png b/capture.png new file mode 100644 index 0000000..a30581c Binary files /dev/null and b/capture.png differ diff --git a/include/cpp-repl/cli/cli.h b/include/cpp-repl/cli/cli.h index 833603c..b13f7c9 100644 --- a/include/cpp-repl/cli/cli.h +++ b/include/cpp-repl/cli/cli.h @@ -5,23 +5,47 @@ namespace cpprepl { namespace cli { +/** + * @file cli.h + * @brief Command-line parsing for cpp-repl. + */ + +/** + * @brief Parsed command-line options. + */ struct Options { - bool showScaffold = false; - bool noInteractive = false; - bool showHelp = false; - bool showVersion = false; - std::vector execCodes; - std::vector files; - // Include / library support (absolute & relative) - std::vector includePaths; // -I - std::vector libraryPaths; // -L - std::vector libraries; // -l - std::vector defines; // -D - std::vector extraArgs; // -- extra clang args + bool showScaffold = false; ///< Show low-level VM IR demo. + bool noInteractive = false; ///< Exit after -e/file execution. + bool showHelp = false; ///< Print help. + bool showVersion = false; ///< Print version. + bool noColor = false; ///< Disable colored prompt/timing. + std::vector execCodes; ///< Code from -e flags. + std::vector files; ///< Input files. + std::vector includePaths; ///< Include paths (-I). + std::vector libraryPaths; ///< Library search paths (-L). + std::vector libraries; ///< Libraries to link (-l). + std::vector defines; ///< Macro definitions (-D). + std::vector extraArgs; ///< Extra clang args. }; +/** + * @brief Parse command-line arguments. + * @param argc Argument count. + * @param argv Argument values. + * @param err Output error message. + * @return Parsed options. + */ Options parse(int argc, char **argv, std::string &err); + +/** + * @brief Print help to stdout. + * @param prog Program name (argv[0]). + */ void printHelp(const char *prog); + +/** + * @brief Print version to stdout. + */ void printVersion(); } // namespace cli diff --git a/include/cpp-repl/core/vm.h b/include/cpp-repl/core/vm.h index 69b685d..12d1f18 100644 --- a/include/cpp-repl/core/vm.h +++ b/include/cpp-repl/core/vm.h @@ -7,20 +7,58 @@ namespace cpprepl { namespace core { -// Abstract VM interface – scalable, pluggable backends. +/** + * @file vm.h + * @brief Low-level VM abstraction for JIT execution. + */ + +/** + * @brief Abstract VM interface for pluggable JIT backends. + * + * Each backend implements module addition and symbol lookup. + * The default backend is LLJITVM. + */ class VM { public: virtual ~VM() = default; + + /** + * @brief Initialize the VM and native target. + * @param err Output error message on failure. + * @return true on success. + */ virtual bool init(std::string &err) = 0; + + /** + * @brief Add an LLVM IR module to the JIT. + * @param M LLVM module to add. + * @param Ctx LLVM context owning the module. + * @param err Output error message on failure. + * @return true on success. + */ virtual bool addModule(std::unique_ptr M, std::unique_ptr Ctx, std::string &err) = 0; + + /** + * @brief Lookup a JIT symbol by name. + * @param name Symbol name as in IR. + * @return Executor address or error. + */ virtual llvm::Expected lookup(const std::string &name) = 0; + + /** + * @brief Get the underlying LLJIT instance if available. + * @return Pointer to LLJIT or nullptr. + */ virtual llvm::orc::LLJIT *getLLJIT() = 0; }; -// Low-level LLJIT implementation – the default VM. -// No optimizations, O0, pure execution. +/** + * @brief LLJIT-based VM implementation. + * + * Uses llvm::orc::LLJIT with O0, no optimizations, for correctness. + */ class LLJITVM : public VM { public: LLJITVM() = default; diff --git a/include/cpp-repl/fix_np_headers.hpp b/include/cpp-repl/fix_np_headers.hpp index 826ba89..de4661a 100644 --- a/include/cpp-repl/fix_np_headers.hpp +++ b/include/cpp-repl/fix_np_headers.hpp @@ -1,7 +1,10 @@ +/** + * @file fix_np_headers.hpp + * @brief Force-included fixes for Numpy-C-API headers. + * + * Handles NZERO/PZERO clashes and ProxyBase compatibility without modifying upstream headers. + */ #pragma once -// Comprehensive fix for Numpy-C-API headers in cpp-repl -// Force-included via -include before any user code -// Handles NZERO/PZERO, vector, ProxyBase without modifying headers // 1. NZERO/PZERO clash #ifdef NZERO diff --git a/include/cpp-repl/fix_proxy.hpp b/include/cpp-repl/fix_proxy.hpp index c896c15..b1b602a 100644 --- a/include/cpp-repl/fix_proxy.hpp +++ b/include/cpp-repl/fix_proxy.hpp @@ -1,6 +1,9 @@ +/** + * @file fix_proxy.hpp + * @brief Patch for np::ProxyBase to support convert_to and operators. + */ #pragma once -// Fix for np::ProxyBase with boost::multiprecision and other types -// Force-included via -include before any user code to patch ProxyBase +/** @brief Force-included fix for ProxyBase with boost::multiprecision. */ #include #include diff --git a/include/cpp-repl/interpreter/interpreter.h b/include/cpp-repl/interpreter/interpreter.h index 4c01e70..f7ac165 100644 --- a/include/cpp-repl/interpreter/interpreter.h +++ b/include/cpp-repl/interpreter/interpreter.h @@ -12,8 +12,18 @@ class Interpreter; namespace cpprepl { namespace interpreter { -// Scalable interpreter wrapper around clang::Interpreter. -// Handles C++ version auto-detection and BigInt preamble injection. +/** + * @file interpreter.h + * @brief Scalable interpreter wrapper around clang::Interpreter. + */ + +/** + * @brief High-level REPL interpreter with version and BigInt support. + * + * Wraps clang::Interpreter (incremental parsing + ORC JIT), auto-detects + * C++ standard, injects BigInt preamble, and handles include/library paths. + * Supports incremental eval without a main function, like Python REPL. + */ class Interpreter { public: Interpreter(); @@ -22,56 +32,135 @@ class Interpreter { Interpreter(const Interpreter &) = delete; Interpreter &operator=(const Interpreter &) = delete; - // Init with explicit version and include/lib options (absolute & relative) + /** + * @brief Init with explicit version and include/lib options. + * @param version Initial C++ standard. + * @param includePaths Include search paths (-I). + * @param defines Macro definitions (-D). + * @param err Output error. + * @return true on success. + */ bool init(utils::StdVersion version, const std::vector &includePaths, const std::vector &defines, std::string &err); + /** + * @brief Init with full options. + * @param version Initial C++ standard. + * @param includePaths Include paths. + * @param defines Defines. + * @param libraryPaths Library search paths (-L). + * @param libraries Libraries to link (-l). + * @param err Output error. + * @return true on success. + */ bool init(utils::StdVersion version, const std::vector &includePaths, const std::vector &defines, const std::vector &libraryPaths, const std::vector &libraries, std::string &err); + /** @brief Init with version only. */ bool init(utils::StdVersion version, std::string &err); + /** @brief Init with C++23 default. */ bool init(std::string &err) { return init(utils::StdVersion::Cpp23, err); } + /** @brief Init with includes/defines and C++23 default. */ bool init(const std::vector &includePaths, const std::vector &defines, std::string &err) { return init(utils::StdVersion::Cpp23, includePaths, defines, err); } - // Dynamic include/library handling (interactive :I, :L) + /** + * @brief Add an include search path at runtime. + * @param path Absolute or relative path. + * @param err Output error. + * @return true on success, re-inits and replays history. + */ bool addIncludePath(const std::string &path, std::string &err); + /** + * @brief Add a library search path at runtime. + * @param path Absolute or relative path. + * @param err Output error. + * @return true on success. + */ bool addLibraryPath(const std::string &path, std::string &err); + /** + * @brief Load a library by name or path. + * @param lib Library name or file path. + * @param err Output error. + * @return true on success. + */ bool addLibrary(const std::string &lib, std::string &err); - // Auto-detect version from code and re-init if needed (scalable) + /** + * @brief Evaluate code with automatic version detection. + * @param code Raw C++ code. + * @param err Output error. + * @return true on success. + */ bool evalAuto(const std::string &code, std::string &err); - // Core eval – python-like: no main needed, raw code + /** + * @brief Evaluate raw C++ code incrementally. + * @param code C++ source without main. + * @param err Output error. + * @return true on success. + */ bool eval(const std::string &code, std::string &err); + /** + * @brief Evaluate with incomplete-input detection. + * @param code Code buffer. + * @param err Output error. + * @param incomplete Set to true if braces/parens are unbalanced. + * @return true if handled. + */ bool eval(const std::string &code, std::string &err, bool &incomplete); + /** @brief Load and execute a raw C++ file. */ bool loadFile(const std::string &path, std::string &err); + /** @brief Load a dynamic library. */ bool loadLibrary(const std::string &path, std::string &err); + /** + * @brief Undo last N inputs. + * @param n Number of PTUs to remove. + * @param err Output error. + * @return true on success. + */ bool undo(unsigned n, std::string &err); + /** @brief Dump history and current version to stdout. */ void dump() const; + /** @brief Reset interpreter state. */ void reset(std::string &err); + /** @brief Print help text. */ void help() const; + /** @brief Number of history entries. */ size_t historySize() const { return history_.size(); } + /** @brief Current C++ standard. */ utils::StdVersion currentVersion() const { return currentVersion_; } private: + /** @brief Ensure at least the needed C++ version, re-init if higher. */ bool ensureVersion(utils::StdVersion needed, std::string &err); + /** @brief Re-init with current options and replay history. */ bool reinitWithCurrentOptions(std::string &err); + /** @brief Sanitize include directives (e.g., strip trailing semicolon). */ std::string sanitizeIncludes(const std::string &code); + /** @brief Reject redefinition with different value. */ bool checkVariableRedefinition(const std::string &code, std::string &err); + /** @brief Track variable declarations for redefinition checks. */ void trackVariable(const std::string &code); + /** @brief Parse a declaration into type/name/value. */ bool parseDeclaration(const std::string &code, std::string &type, std::string &name, std::string &value); + /** @brief Parse an assignment into name/value. */ bool parseAssignment(const std::string &code, std::string &name, std::string &value); + /** @brief Normalize a value string for comparison. */ std::string normalizeValue(const std::string &v); + /** @brief Ensure standard library is available (bits/stdc++.h). */ + bool ensureStdLib(std::string &err); + /** @brief Try to include bits/stdc++.h or fallback headers. */ + bool tryIncludeStdLib(); std::unique_ptr interp_; bool initialized_ = false; std::vector history_; @@ -80,10 +169,10 @@ class Interpreter { std::vector defines_; std::vector libraryPaths_; std::vector libraries_; - // Storage for compiler args c_str() lifetime std::vector compilerArgsStorage_; std::unordered_map> variables_; std::vector>> varHistory_; + bool stdLibIncluded_ = false; }; } // namespace interpreter diff --git a/include/cpp-repl/repl/session.h b/include/cpp-repl/repl/session.h index 0801be7..1e350a4 100644 --- a/include/cpp-repl/repl/session.h +++ b/include/cpp-repl/repl/session.h @@ -5,25 +5,75 @@ namespace cpprepl { namespace repl { -// Scalable REPL session – handles prompts, multiline, commands. -// Like Python's REPL, but for C++ raw code. +/** + * @file session.h + * @brief Interactive REPL session handling. + */ + +/** + * @brief Manages the interactive prompt, history, and commands. + * + * Delegates execution to interpreter::Interpreter and provides Python-like + * REPL behavior for raw C++ input. + */ class Session { public: + /** + * @brief Construct a session bound to an interpreter. + * @param interp Reference to initialized interpreter. + */ explicit Session(interpreter::Interpreter &interp); ~Session() = default; - // Run interactive loop reading from stdin + /** + * @brief Run the interactive loop reading from stdin. + * + * Handles prompts cpp> / ...>, line buffering, and :commands. + */ void runInteractive(); - // Execute single line (used for -e and tests) + /** + * @brief Execute a single code snippet. + * @param code Raw C++ code. + * @param err Output error message. + * @return true on success. + */ bool exec(const std::string &code, std::string &err); private: + /** + * @brief Handle a colon command like :help or :I. + * @param line Input line. + * @param err Output error. + * @return true if line was a command. + */ bool handleCommand(const std::string &line, std::string &err); + /** + * @brief Check if buffer has unbalanced braces/parens. + * @param buffer Current input buffer. + * @return true if more input is needed. + */ bool isIncomplete(const std::string &buffer) const; - interpreter::Interpreter &interp_; - std::string buffer_; + /** @brief Whether to use ANSI color for prompt. */ + bool shouldUseColor(bool forReadline) const; + /** @brief Format duration in ms for display. */ + std::string formatDuration(double ms) const; + /** @brief Build primary prompt string. */ + std::string buildPrimaryPrompt(bool forReadline) const; + /** @brief Build continuation prompt string. */ + std::string buildContinuationPrompt(bool forReadline) const; + /** @brief Print timing line after execution. */ + void printTimingLine(bool success, double ms) const; + /** @brief Print highlighted echo of executed code (if color enabled). */ + void printHighlightedEcho(const std::string &code) const; + + interpreter::Interpreter &interp_; ///< Bound interpreter instance. + std::string buffer_; ///< Current multiline buffer. + int promptCount_ = 1; ///< Prompt counter for numbered prompts. + double lastDurationMs_ = 0.0; ///< Last execution time in ms. + bool lastSuccess_ = true; ///< Last execution success flag. + bool hasLastTiming_ = false; ///< Whether timing is available. }; } // namespace repl diff --git a/include/cpp-repl/utils/bigint.h b/include/cpp-repl/utils/bigint.h index facd5b8..fb24ef5 100644 --- a/include/cpp-repl/utils/bigint.h +++ b/include/cpp-repl/utils/bigint.h @@ -4,22 +4,41 @@ namespace cpprepl { namespace utils { -// Big integer support – like Python's arbitrary large ints. -// Uses boost::multiprecision::cpp_int (header-only) and GMP if available. -// Scalable: header provides preamble to inject into REPL. +/** + * @file bigint.h + * @brief Big integer support via boost::multiprecision. + */ + +/** + * @brief Provides BigInt preamble and helpers for the REPL. + * + * Injects boost::multiprecision headers so users can write cpp_int / bigint + * directly without manual includes, similar to Python's big ints. + */ class BigIntSupport { public: - // Preamble injected at REPL init for transparent bigint support. - // Users can then write: bigint x = 12345678901234567890_cpp_int; or use cpp_int directly. + /** + * @brief Preamble with cpp_int and bigint alias. + * @return C++ code injected at interpreter init. + */ static const char *preamble(); - // Alternative GMP preamble (uses mpz_int which wraps libgmp) + /** + * @brief GMP-specific preamble (mpz_int). + * @return C++ code for GMP backend. + */ static const char *gmpPreamble(); - // Check if boost headers are available (always true in our build) + /** + * @brief Check if boost headers are available. + * @return true if usable. + */ static bool isAvailable(); - // Example large number for testing + /** + * @brief Demo code for documentation and tests. + * @return Example snippet using big ints. + */ static std::string demoCode(); }; diff --git a/include/cpp-repl/utils/highlight.h b/include/cpp-repl/utils/highlight.h new file mode 100644 index 0000000..484c8dc --- /dev/null +++ b/include/cpp-repl/utils/highlight.h @@ -0,0 +1,22 @@ +#pragma once +#include + +namespace cpprepl { +namespace utils { + +/** + * @brief Simple C++ syntax highlighter for REPL output. + * + * Colors keywords, types, preprocessor, strings, comments, numbers. + * Respects NO_COLOR / --no-color via shouldUseColor check externally, + * but this function itself just checks useColor param. + */ +class Highlighter { +public: + static std::string highlight(const std::string &code, bool useColor); + static std::string highlightType(const std::string &typeStr, bool useColor); + static std::string highlightValue(const std::string &valStr, bool useColor); +}; + +} // namespace utils +} // namespace cpprepl diff --git a/include/cpp-repl/utils/version_detector.h b/include/cpp-repl/utils/version_detector.h index b69a6c2..e2c009c 100644 --- a/include/cpp-repl/utils/version_detector.h +++ b/include/cpp-repl/utils/version_detector.h @@ -5,20 +5,37 @@ namespace cpprepl { namespace utils { +/** + * @file version_detector.h + * @brief Automatic C++ standard detection. + */ + +/** + * @brief Supported C++ standards. + */ enum class StdVersion { - Cpp17 = 17, - Cpp20 = 20, - Cpp23 = 23 + Cpp17 = 17, ///< C++17 + Cpp20 = 20, ///< C++20 + Cpp23 = 23 ///< C++23 }; +/** + * @brief Detects required C++ standard from source keywords. + */ class VersionDetector { public: - // Auto-detect required C++ standard from code keywords. - // Like python's future imports, but for C++. + /** + * @brief Detect standard needed for given code. + * @param code Source snippet to inspect. + * @return Required StdVersion (minimum is Cpp17). + */ static StdVersion detect(const std::string &code); + /** @brief Convert version to compiler flag, e.g. -std=c++20. */ static std::string toFlag(StdVersion v); + /** @brief Convert version to human string, e.g. C++20. */ static std::string toString(StdVersion v); + /** @brief Human description with features. */ static std::string describe(StdVersion v); private: @@ -26,7 +43,9 @@ class VersionDetector { static bool containsWord(const std::string &code, const std::string &word); }; -// Helper to get current interpreter version as string for diagnostics +/** + * @brief Return the maximum of two versions. + */ StdVersion maxVersion(StdVersion a, StdVersion b); } // namespace utils diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index df546fb..867eb19 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -1,3 +1,7 @@ +/** + * @file cli.cpp + * @brief CLI parsing implementation for cpp-repl. + */ #include "cpp-repl/cli/cli.h" #include @@ -29,6 +33,10 @@ Options parse(int argc, char **argv, std::string &err) { opts.showScaffold = false; else if (arg == "--no-interactive") opts.noInteractive = true; + else if (arg == "--no-color" || arg == "--nocolor" || arg == "--color=never" || arg == "--color=none") + opts.noColor = true; + else if (arg == "--color" || arg == "--color=always" || arg == "--color=auto") + opts.noColor = false; else if (arg == "-e") { if (i + 1 >= argc) { err = "-e requires argument"; return opts; } opts.execCodes.push_back(argv[++i]); @@ -103,6 +111,8 @@ void printHelp(const char *prog) { " -v, --version show version\n" " --scaffold show low-level VM scaffold\n" " --no-interactive exit after file/-e execution\n" + " --no-color disable colored prompt & timing\n" + " --color[=when] force color (always/auto/never)\n" " -e execute raw C++ code\n" " -I , -I add include search path (absolute or relative)\n" " -L , -L add library search path\n" @@ -114,6 +124,8 @@ void printHelp(const char *prog) { "REPL: :help :dump :reset :load :lib :I :L :version :quit\n" " :I add include path (interactive)\n" " :L add library path\n" + "Prompt: colored cpp:C++17/20/23 [n] (time ✓/✗)> with execution time\n" + " honor NO_COLOR / CPP_REPL_NO_COLOR=1 and --no-color, TERM=dumb\n" "C++ versions: auto-detects C++20/23 keywords " "(concept/requires/import)\n" "BigInt: boost::multiprecision::cpp_int / bigint\n" diff --git a/src/core/vm.cpp b/src/core/vm.cpp index db74c28..7ede638 100644 --- a/src/core/vm.cpp +++ b/src/core/vm.cpp @@ -1,3 +1,7 @@ +/** + * @file vm.cpp + * @brief LLJITVM implementation - thin wrapper over llvm::orc::LLJIT. + */ #include "cpp-repl/core/vm.h" #include "llvm/Support/Error.h" #include "llvm/Support/TargetSelect.h" diff --git a/src/interpreter/interpreter.cpp b/src/interpreter/interpreter.cpp index c594743..243ef02 100644 --- a/src/interpreter/interpreter.cpp +++ b/src/interpreter/interpreter.cpp @@ -1,17 +1,165 @@ +/** + * @file interpreter.cpp + * @brief Interpreter implementation with high-precision float printing and BigInt handling. + */ #include "cpp-repl/interpreter/interpreter.h" #include "cpp-repl/utils/bigint.h" +#include "cpp-repl/utils/highlight.h" #include "cpp-repl/utils/version_detector.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Interpreter/Interpreter.h" #include "llvm/Support/Error.h" #include "llvm/Support/TargetSelect.h" #include "llvm/Support/raw_ostream.h" +#include "clang/AST/Type.h" #include #include #include #include #include #include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +namespace { +/** + * @brief Helper for high-precision floating point printing. + * + * Replaces clang's default %.6g/%.8g which truncates to 6-8 digits. + * Uses max_digits10 (9 for float, 17 for double, 21 for long double) + * so printed values round-trip and preserve input precision. + */ +static std::string formatFloatHighPrec(float v) { + std::string out; + llvm::raw_string_ostream ss(out); + if (std::isnan(v) || std::isinf(v)) { + ss << llvm::format("%g", v); + } else if (v == static_cast(static_cast(v))) { + ss << llvm::format("%.1f", v); + } else { + ss << llvm::format("%#.9g", v); + } + ss << 'f'; + return ss.str(); +} +static std::string formatDoubleHighPrec(double v) { + std::string out; + llvm::raw_string_ostream ss(out); + if (std::isnan(v) || std::isinf(v)) { + ss << llvm::format("%g", v); + } else if (v == static_cast(static_cast(v))) { + ss << llvm::format("%.1f", v); + } else { + ss << llvm::format("%#.17g", v); + } + return ss.str(); +} +static std::string formatLongDoubleHighPrec(long double v) { + std::string out; + llvm::raw_string_ostream ss(out); + if (std::isnan(v) || std::isinf(v)) { + ss << llvm::format("%Lg", v); + } else if (v == static_cast(static_cast(v))) { + ss << llvm::format("%.1Lf", v); + } else { + constexpr int prec = std::numeric_limits::max_digits10; + std::string fmt = "%#." + std::to_string(prec) + "Lg"; + ss << llvm::format(fmt.c_str(), v); + } + ss << 'L'; + return ss.str(); +} + +static void highPrecisionDump(const clang::Value &V) { + if (!V.isValid() || V.isVoid()) + return; + std::string typeStr; + { + llvm::raw_string_ostream ts(typeStr); + V.printType(ts); + } + std::string dataStr; + bool handled = false; + // Try direct Kind first – covers most prvalues + switch (V.getKind()) { + case clang::Value::K_Float: + dataStr = formatFloatHighPrec(V.getFloat()); + handled = true; + break; + case clang::Value::K_Double: + dataStr = formatDoubleHighPrec(V.getDouble()); + handled = true; + break; + case clang::Value::K_LongDouble: + dataStr = formatLongDoubleHighPrec(V.getLongDouble()); + handled = true; + break; + default: + break; + } + if (!handled) { + // Fallback: check QualType for reference / object cases where Kind is + // K_PtrOrObj but the underlying type is a builtin floating type. + clang::QualType qt = V.getType(); + clang::QualType nonRef = qt.getNonReferenceType(); + const clang::Type *canon = nonRef.getCanonicalType().getTypePtr(); + if (auto *bt = llvm::dyn_cast(canon)) { + if (bt->getKind() == clang::BuiltinType::Float || + bt->getKind() == clang::BuiltinType::Double || + bt->getKind() == clang::BuiltinType::LongDouble) { + // Value is stored as a pointer (reference / object) + if (V.getKind() == clang::Value::K_PtrOrObj && V.getPtr()) { + void *p = V.getPtr(); + if (bt->getKind() == clang::BuiltinType::Float) { + float fv = *static_cast(p); + dataStr = formatFloatHighPrec(fv); + handled = true; + } else if (bt->getKind() == clang::BuiltinType::Double) { + double dv = *static_cast(p); + dataStr = formatDoubleHighPrec(dv); + handled = true; + } else { + long double ldv = *static_cast(p); + dataStr = formatLongDoubleHighPrec(ldv); + handled = true; + } + } + } + } + } + if (!handled) { + llvm::raw_string_ostream ds(dataStr); + V.printData(ds); + } + // Keyword highlight: colorize type and value when tty and color enabled + bool useColor = false; +#ifndef _WIN32 + useColor = isatty(STDOUT_FILENO) && !getenv("NO_COLOR") && !getenv("CPP_REPL_NO_COLOR") && !getenv("NO_COLOUR"); + if (useColor) { + const char *term = getenv("TERM"); + if (term && std::string(term)=="dumb") useColor=false; + } + if (getenv("FORCE_COLOR") || getenv("CLICOLOR_FORCE")) useColor = true; +#else + useColor = false; +#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; + } else { + llvm::outs() << "(" << typeStr << ") " << dataStr << "\n"; + } +} +} // namespace namespace cpprepl { namespace interpreter { @@ -45,6 +193,7 @@ bool Interpreter::init(utils::StdVersion version, libraryPaths_ = libraryPaths; libraries_ = libraries; currentVersion_ = version; + stdLibIncluded_ = false; clang::IncrementalCompilerBuilder builder; compilerArgsStorage_.clear(); @@ -150,6 +299,9 @@ bool Interpreter::init(utils::StdVersion version, llvm::handleAllErrors(std::move(e2), [&](llvm::ErrorInfoBase &EIB) {}); #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_) { auto tryLoad = [&](const std::string &path) -> bool { @@ -209,6 +361,7 @@ bool Interpreter::reinitWithCurrentOptions(std::string &err) { history_.clear(); variables_.clear(); varHistory_.clear(); + stdLibIncluded_ = false; if (!init(currentVersion_, includePaths_, defines_, libraryPaths_, libraries_, local)) { err = local; return false; @@ -285,6 +438,7 @@ bool Interpreter::ensureVersion(utils::StdVersion needed, std::string &err) { history_.clear(); variables_.clear(); varHistory_.clear(); + stdLibIncluded_ = false; if (!init(needed, includePaths_, defines_, libraryPaths_, libraries_, local)) { err = local; return false; @@ -298,6 +452,46 @@ bool Interpreter::ensureVersion(utils::StdVersion needed, std::string &err) { return true; } +bool Interpreter::tryIncludeStdLib() { + if (stdLibIncluded_ || !initialized_ || !interp_) return stdLibIncluded_; + clang::Value V; + // Try bits/stdc++.h first (covers everything) + auto e = interp_->ParseAndExecute("#include \n", &V); + if (!e) { stdLibIncluded_ = true; return true; } + llvm::handleAllErrors(std::move(e), [&](llvm::ErrorInfoBase &EIB){}); + // Fallback: include common STL headers individually + const char *fallback = R"( +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +)"; + auto e2 = interp_->ParseAndExecute(fallback, &V); + if (!e2) { stdLibIncluded_ = true; return true; } + llvm::handleAllErrors(std::move(e2), [&](llvm::ErrorInfoBase &EIB){}); + return false; +} + +bool Interpreter::ensureStdLib(std::string &err) { + if (stdLibIncluded_) return true; + if (!initialized_) { err = "REPL not initialized"; return false; } + if (tryIncludeStdLib()) { err.clear(); return true; } + err = "failed to include standard library (bits/stdc++.h)"; + return false; +} + bool Interpreter::evalAuto(const std::string &code, std::string &err) { auto needed = utils::VersionDetector::detect(code); if (!ensureVersion(needed, err)) @@ -571,6 +765,12 @@ bool Interpreter::eval(const std::string &code, std::string &err) { if (trimmed.empty()) return true; + // Proactive stdlib: include on first use of std:: + if (!stdLibIncluded_ && sanitized.find("std::") != std::string::npos) { + std::string dummy; + ensureStdLib(dummy); + } + if (trimmed.rfind("#include", 0) == 0) { size_t q1 = sanitized.find('"'); size_t q2 = std::string::npos; @@ -643,7 +843,7 @@ bool Interpreter::eval(const std::string &code, std::string &err) { auto e2 = interp_->ParseAndExecute(sanitized, &V2); if (!e2) { if (V2.isValid()) { - V2.dump(); + highPrecisionDump(V2); std::cout << "\n"; } if (!sanitized.empty()) { @@ -661,7 +861,7 @@ bool Interpreter::eval(const std::string &code, std::string &err) { auto e2 = interp_->ParseAndExecute(withSemi, &V2); if (!e2) { if (V2.isValid()) { - V2.dump(); + highPrecisionDump(V2); std::cout << "\n"; } if (!sanitized.empty()) { @@ -699,7 +899,7 @@ bool Interpreter::eval(const std::string &code, std::string &err) { bool shouldPrint2 = true; if (V2.isVoid()) shouldPrint2 = false; else if (trimmed.find("std::cout") != std::string::npos) shouldPrint2 = false; - if (shouldPrint2) { V2.dump(); std::cout << "\n"; } + if (shouldPrint2) { highPrecisionDump(V2); std::cout << "\n"; } } if (!sanitized.empty()) { history_.push_back(sanitized); @@ -762,6 +962,54 @@ bool Interpreter::eval(const std::string &code, std::string &err) { "or use static_cast(proxy).convert_to() and " "static_cast(a[n]) for ap*a[n]"; } + // Auto-include standard library if keyword suggests missing header + // e.g., std::exp, std::forward, std::vector without prior include + if (!stdLibIncluded_ && sanitized.find("std::") != std::string::npos && + (msg.find("undeclared identifier") != std::string::npos || + msg.find("no member named") != std::string::npos || + msg.find("has no member") != std::string::npos || + msg.find("unknown type name") != std::string::npos || + msg.find("use of undeclared") != std::string::npos || + msg.find("implicit instantiation") != std::string::npos)) { + std::string dummy; + if (ensureStdLib(dummy)) { + clang::Value V2; + auto e2 = interp_->ParseAndExecute(toEval, &V2); + if (!e2) { + if (V2.isValid()) { + bool shouldPrint = true; + if (V2.isVoid()) shouldPrint = false; + else if (trimmed.find("std::cout") != std::string::npos) shouldPrint = false; + if (shouldPrint) { highPrecisionDump(V2); std::cout << "\n"; } + } + if (!sanitized.empty()) { + history_.push_back(sanitized); + trackVariable(sanitized); + } + std::cout << "[auto-included for std:: support]\n"; + return true; + } + llvm::handleAllErrors(std::move(e2), [&](llvm::ErrorInfoBase &EIB){ msg = EIB.message(); }); + msg += "\n[hint] tried auto-including but still failed; try explicit #include <...> or check std:: usage"; + } + } + // JIT poison recovery: Symbols not found / Failed to materialize symbols + if (msg.find("Symbols not found") != std::string::npos || + msg.find("Failed to materialize symbols") != std::string::npos || + msg.find("__orc_init_func") != std::string::npos) { + // Attempt to undo the poisoned increment; clang::Interpreter::Undo(1) often clears it + if (auto ue = interp_->Undo(1)) { + llvm::handleAllErrors(std::move(ue), [&](llvm::ErrorInfoBase &EIB){}); + } + // Also pop last history if it was the poisoned one (if any) + // No push yet for this failed eval, so nothing to pop from history, but prior poisoned def may be in history + // Offer hint and suggest :reset if still broken + if (msg.find("Failed to materialize") != std::string::npos) { + msg += "\n[hint] JIT poisoned — auto-undo attempted. If subsequent #include still fails, run :reset or restart. Original error was likely a bad template (e.g., std::forward without )."; + } else { + msg += "\n[hint] Symbols not found — likely a failed template instantiation (e.g., std::forward(x) should be std::forward(x)). Auto-undo attempted; try :undo or :reset."; + } + } err = msg; return false; } @@ -799,7 +1047,7 @@ bool Interpreter::eval(const std::string &code, std::string &err) { } } if (shouldPrint) { - V.dump(); + highPrecisionDump(V); std::cout << "\n"; } } else { @@ -847,7 +1095,7 @@ bool Interpreter::eval(const std::string &code, std::string &err) { llvm::handleAllErrors(std::move(e3), [&](llvm::ErrorInfoBase &EIB) {}); } else { - V2.dump(); + highPrecisionDump(V2); std::cout << "\n"; } } else if (e2) @@ -964,15 +1212,27 @@ void Interpreter::reset(std::string &err) { history_.clear(); variables_.clear(); varHistory_.clear(); + stdLibIncluded_ = false; if (!init(currentVersion_, local)) err = local; else err.clear(); } void Interpreter::help() const { + // Show prompt help with color hint when stdout is a tty + bool useColor = isatty(STDOUT_FILENO) && + !getenv("NO_COLOR") && !getenv("CPP_REPL_NO_COLOR"); + const char *term = getenv("TERM"); + if (useColor && term && std::string(term) == "dumb") useColor = false; + auto col = [&](const char* code)->std::string { return useColor ? code : ""; }; + auto rst = col("\033[0m"); + auto cyan = col("\033[36m"); + auto grey = col("\033[90m"); std::cout << "C++ REPL (LLVM VM, O0, no optimizations) [" << utils::VersionDetector::toString(currentVersion_) << "]\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" " :help :h show this help\n" " :quit :exit :q exit REPL\n" @@ -1005,7 +1265,8 @@ void Interpreter::help() const { "C++20/23: auto-detects 'concept', 'requires', 'import' etc. " "and switches to -std=c++20/23\n" "\n" - "Multiline: unbalanced { ( [ keeps buffering with ...> prompt\n"; + "Multiline: unbalanced { ( [ keeps buffering with " + col("\033[33m") + "...>" + rst + " prompt\n" + "Timing: " + grey + "⏱" + rst + " line after each exec + inline in next prompt (e.g. " + grey + "(12.3ms ✓)" + rst + ")\n"; } } // namespace interpreter diff --git a/src/main.cpp b/src/main.cpp index 30af5b5..0cec16e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,7 @@ +/** + * @file main.cpp + * @brief Entry point for cpp-repl - CLI, VM scaffold, and REPL session. + */ #include "cpp-repl/cli/cli.h" #include "cpp-repl/core/vm.h" #include "cpp-repl/interpreter/interpreter.h" @@ -16,6 +20,10 @@ #include #include +#include +#ifndef _WIN32 +#include +#endif int main(int argc, char **argv) { std::string cliErr; @@ -122,7 +130,25 @@ int main(int argc, char **argv) { if (opts.noInteractive) return 0; - // Show help and current capabilities + // Honor --no-color early (sets env for Session::shouldUseColor) + if (opts.noColor) { + setenv("CPP_REPL_NO_COLOR", "1", 1); + setenv("NO_COLOR", "1", 1); + } + + // Show help and current capabilities (with subtle color when enabled) + { + bool useColor = !opts.noColor && isatty(STDOUT_FILENO) && + !getenv("NO_COLOR") && !getenv("CPP_REPL_NO_COLOR"); + const char *term = getenv("TERM"); + if (useColor && term && std::string(term) == "dumb") useColor = false; + if (useColor) { + std::cout << "\033[90m— cpp-repl \033[1;36mC++ REPL\033[0m\033[90m " + "(LLVM 22, O0) — type \033[33m:help\033[90m, " + "\033[33m:quit\033[90m, prompt shows \033[36mcpp:C++\033[90m" + " version, [n] and \033[32m⏱ time\033[90m —\033[0m\n"; + } + } interp.help(); std::cout << "BigInt available: " << (cpprepl::utils::BigIntSupport::isAvailable() diff --git a/src/repl.cpp b/src/repl.cpp index 3613771..c61edff 100644 --- a/src/repl.cpp +++ b/src/repl.cpp @@ -1,3 +1,7 @@ +/** + * @file repl.cpp + * @brief Legacy REPL wrapper (kept for compatibility) with high-precision printing. + */ #include "repl.h" #include "clang/Frontend/CompilerInstance.h" @@ -5,11 +9,75 @@ #include "llvm/Support/Error.h" #include "llvm/Support/TargetSelect.h" #include "llvm/Support/raw_ostream.h" +#include "clang/AST/Type.h" #include #include #include #include #include +#include +#include + +namespace { +static std::string formatFloatHighPrec(float v) { + std::string out; + llvm::raw_string_ostream ss(out); + if (std::isnan(v) || std::isinf(v)) ss << llvm::format("%g", v); + else if (v == static_cast(static_cast(v))) ss << llvm::format("%.1f", v); + else ss << llvm::format("%#.9g", v); + ss << 'f'; + return ss.str(); +} +static std::string formatDoubleHighPrec(double v) { + std::string out; + llvm::raw_string_ostream ss(out); + if (std::isnan(v) || std::isinf(v)) ss << llvm::format("%g", v); + else if (v == static_cast(static_cast(v))) ss << llvm::format("%.1f", v); + else ss << llvm::format("%#.17g", v); + return ss.str(); +} +static std::string formatLongDoubleHighPrec(long double v) { + std::string out; + llvm::raw_string_ostream ss(out); + if (std::isnan(v) || std::isinf(v)) ss << llvm::format("%Lg", v); + else if (v == static_cast(static_cast(v))) ss << llvm::format("%.1Lf", v); + else { + constexpr int prec = std::numeric_limits::max_digits10; + std::string fmt = "%#." + std::to_string(prec) + "Lg"; + ss << llvm::format(fmt.c_str(), v); + } + ss << 'L'; + return ss.str(); +} +static void highPrecisionDump(const clang::Value &V) { + if (!V.isValid() || V.isVoid()) return; + std::string typeStr; + { llvm::raw_string_ostream ts(typeStr); V.printType(ts); } + std::string dataStr; + bool handled = false; + switch (V.getKind()) { + case clang::Value::K_Float: dataStr = formatFloatHighPrec(V.getFloat()); handled = true; break; + case clang::Value::K_Double: dataStr = formatDoubleHighPrec(V.getDouble()); handled = true; break; + case clang::Value::K_LongDouble: dataStr = formatLongDoubleHighPrec(V.getLongDouble()); handled = true; break; + default: break; + } + if (!handled) { + clang::QualType qt = V.getType(); + clang::QualType nonRef = qt.getNonReferenceType(); + const clang::Type *canon = nonRef.getCanonicalType().getTypePtr(); + if (auto *bt = llvm::dyn_cast(canon)) { + if ((bt->getKind() == clang::BuiltinType::Float || bt->getKind() == clang::BuiltinType::Double || bt->getKind() == clang::BuiltinType::LongDouble) && V.getKind() == clang::Value::K_PtrOrObj && V.getPtr()) { + void *p = V.getPtr(); + if (bt->getKind() == clang::BuiltinType::Float) { dataStr = formatFloatHighPrec(*static_cast(p)); handled = true; } + else if (bt->getKind() == clang::BuiltinType::Double) { dataStr = formatDoubleHighPrec(*static_cast(p)); handled = true; } + else { dataStr = formatLongDoubleHighPrec(*static_cast(p)); handled = true; } + } + } + } + if (!handled) { llvm::raw_string_ostream ds(dataStr); V.printData(ds); } + llvm::outs() << "(" << typeStr << ") " << dataStr << "\n"; +} +} // namespace namespace repl { @@ -124,7 +192,7 @@ bool Repl::eval(const std::string &code, std::string &err) { auto e2 = interp_->ParseAndExecute(code, &V2); if (!e2) { if (V2.isValid()) { - V2.dump(); + highPrecisionDump(V2); std::cout << "\n"; } if (!code.empty()) @@ -142,7 +210,7 @@ bool Repl::eval(const std::string &code, std::string &err) { auto e2 = interp_->ParseAndExecute(withSemi, &V2); if (!e2) { if (V2.isValid()) { - V2.dump(); + highPrecisionDump(V2); std::cout << "\n"; } if (!code.empty()) @@ -169,7 +237,7 @@ bool Repl::eval(const std::string &code, std::string &err) { shouldPrint = false; } if (shouldPrint) { - V.dump(); + highPrecisionDump(V); std::cout << "\n"; } } else { @@ -199,7 +267,7 @@ bool Repl::eval(const std::string &code, std::string &err) { clang::Value V2; auto e2 = interp_->ParseAndExecute(stripped, &V2); if (!e2 && V2.isValid()) { - V2.dump(); + highPrecisionDump(V2); std::cout << "\n"; // Undo the just-executed expression's PTU side-effect? We already // executed original "x;" which was no-op value, so duplicate diff --git a/src/repl.h b/src/repl.h index 11eed7b..b31d22f 100644 --- a/src/repl.h +++ b/src/repl.h @@ -1,3 +1,7 @@ +/** + * @file repl.h + * @brief Legacy high-level REPL wrapper. + */ #ifndef CPP_REPL_REPL_H #define CPP_REPL_REPL_H @@ -11,8 +15,11 @@ class Interpreter; namespace repl { -/// High-level C++ REPL built on top of the low-level VM (clang::Interpreter -/// which internally uses llvm::orc::LLJIT). No optimization, correctness first. +/** + * @brief High-level C++ REPL built on clang::Interpreter. + * + * Uses llvm::orc::LLJIT internally, O0 and correctness first. + */ class Repl { public: Repl(); @@ -21,28 +28,40 @@ class Repl { Repl(const Repl &) = delete; Repl &operator=(const Repl &) = delete; + /** @brief Initialize the interpreter. */ bool init(std::string &err); - // Execute a single REPL line / block. If Value is produced, it is printed. - // Returns false on error, true on success. + /** + * @brief Execute a REPL line or block and print Value if produced. + * @param code Input code. + * @param err Output error. + * @return true on success. + */ bool eval(const std::string &code, std::string &err); - // REPL commands + /** @brief Dump history. */ void dump() const; + /** @brief Reset state. */ void reset(std::string &err); + /** @brief Print help. */ void help() const; - // Load and execute a file's contents + /** @brief Load and execute a file. */ bool loadFile(const std::string &path, std::string &err); - // Dynamic library loading (wraps Interpreter::LoadDynamicLibrary) + /** @brief Load a dynamic library. */ bool loadLibrary(const std::string &path, std::string &err); - // Undo last N inputs (wraps Interpreter::Undo) + /** @brief Undo last N inputs. */ bool undo(unsigned n, std::string &err); - // Try incremental eval: returns true if code was incomplete and should - // continue buffering (instead of error). Used by REPL loop. + /** + * @brief Eval with incomplete detection for REPL loop. + * @param code Input code. + * @param err Output error. + * @param incomplete Set if more input is needed. + * @return true if handled. + */ bool eval(const std::string &code, std::string &err, bool &incomplete); size_t historySize() const { return history_.size(); } diff --git a/src/repl/session.cpp b/src/repl/session.cpp index 1d5a9e2..2506f85 100644 --- a/src/repl/session.cpp +++ b/src/repl/session.cpp @@ -1,8 +1,23 @@ +/** + * @file session.cpp + * @brief Interactive session loop and command handling. + */ #include "cpp-repl/repl/session.h" +#include "cpp-repl/utils/highlight.h" +#include "cpp-repl/utils/version_detector.h" +#include +#include +#include #include +#include #include #include +#ifdef _WIN32 +#include +#include +#else #include +#endif #ifdef HAS_READLINE #include #include @@ -14,7 +29,159 @@ namespace repl { Session::Session(interpreter::Interpreter &interp) : interp_(interp) {} bool Session::exec(const std::string &code, std::string &err) { - return interp_.eval(code, err); + auto t0 = std::chrono::steady_clock::now(); + bool ok = interp_.eval(code, err); + auto t1 = std::chrono::steady_clock::now(); + lastDurationMs_ = std::chrono::duration(t1 - t0).count(); + lastSuccess_ = ok; + hasLastTiming_ = true; + ++promptCount_; + return ok; +} + +// ── Prompt helpers ────────────────────────────────────────────────────────── +bool Session::shouldUseColor(bool /*forReadline*/) const { + if (std::getenv("NO_COLOR") || std::getenv("CPP_REPL_NO_COLOR") || + std::getenv("NO_COLOUR")) + return false; + if (std::getenv("FORCE_COLOR") || std::getenv("CLICOLOR_FORCE")) + return true; + const char *term = std::getenv("TERM"); + if (term && std::string(term) == "dumb") + return false; +#ifdef _WIN32 + return _isatty(_fileno(stdout)) != 0; +#else + return isatty(STDOUT_FILENO) != 0; +#endif +} + +std::string Session::formatDuration(double ms) const { + std::ostringstream oss; + oss << std::fixed << std::setprecision(2); + if (ms < 1.0) { + oss << std::setprecision(2) << ms << "ms"; + } else if (ms < 1000.0) { + oss << std::setprecision(1) << ms << "ms"; + } else if (ms < 60000.0) { + oss << std::setprecision(2) << (ms / 1000.0) << "s"; + } else { + int totalSec = static_cast(ms / 1000.0); + int minutes = totalSec / 60; + double secs = (ms / 1000.0) - minutes * 60; + oss << minutes << "m" << std::fixed << std::setprecision(1) << secs << "s"; + } + return oss.str(); +} + +std::string Session::buildPrimaryPrompt(bool forReadline) const { + bool color = shouldUseColor(forReadline); + auto wrap = [&](const char *code) -> std::string { + if (!color) return ""; + if (forReadline) return std::string("\001") + code + "\002"; + return std::string(code); + }; + const std::string RST = wrap("\033[0m"); + const std::string BOLD_CYAN = wrap("\033[1;36m"); + const std::string DIM = wrap("\033[2m"); + const std::string DIM_GREY = wrap("\033[90m"); + const std::string YELLOW = wrap("\033[33m"); + const std::string GREEN = wrap("\033[32m"); + const std::string RED = wrap("\033[31m"); + + std::string verStr = utils::VersionDetector::toString(interp_.currentVersion()); + std::string verCol; + if (interp_.currentVersion() == utils::StdVersion::Cpp23) + verCol = wrap("\033[1;35m"); + else if (interp_.currentVersion() == utils::StdVersion::Cpp20) + verCol = YELLOW; + else + verCol = DIM_GREY; + + std::string out; + if (color) { + out += BOLD_CYAN + "cpp" + RST; + out += DIM_GREY + ":" + RST + verCol + verStr + RST; + out += " " + DIM + "[" + std::to_string(promptCount_) + "]" + RST; + if (hasLastTiming_) { + std::string t = formatDuration(lastDurationMs_); + std::string statusCol = lastSuccess_ ? GREEN : RED; + std::string sym = lastSuccess_ ? "✓" : "✗"; + out += " " + DIM_GREY + "(" + RST + statusCol + t + " " + sym + RST + DIM_GREY + ")" + RST; + } + out += DIM_GREY + ">" + RST + " "; + } else { + out = "cpp:" + verStr + " [" + std::to_string(promptCount_) + "]"; + if (hasLastTiming_) { + out += " (" + formatDuration(lastDurationMs_) + (lastSuccess_ ? " ok" : " err") + ")"; + } + out += "> "; + } + return out; +} + +std::string Session::buildContinuationPrompt(bool forReadline) const { + bool color = shouldUseColor(forReadline); + auto wrap = [&](const char *code) -> std::string { + if (!color) return ""; + if (forReadline) return std::string("\001") + code + "\002"; + return std::string(code); + }; + const std::string RST = wrap("\033[0m"); + const std::string DIM_YELLOW = wrap("\033[2;33m"); + if (color) { + return DIM_YELLOW + "...>" + RST + " "; + } else { + return "...> "; + } +} + +void Session::printTimingLine(bool success, double ms) const { +#ifdef _WIN32 + bool isTTY = _isatty(_fileno(stdout)) != 0; +#else + bool isTTY = isatty(STDOUT_FILENO) != 0; +#endif + if (!isTTY) return; + bool color = shouldUseColor(false); + std::string t = formatDuration(ms); + if (color) { + const char *grey = "\033[90m"; + const char *green = "\033[32m"; + const char *red = "\033[31m"; + const char *rst = "\033[0m"; + const char *symCol = success ? green : red; + const char *sym = success ? "✓" : "✗"; + std::cout << grey << "⏱ " << t << " " << symCol << sym << grey << rst << "\n"; + } else { + std::cout << "⏱ " << t << (success ? " ok" : " err") << "\n"; + } + std::cout << std::flush; +} + +void Session::printHighlightedEcho(const std::string &code) const { +#ifdef _WIN32 + bool isTTY = _isatty(_fileno(stdout)) != 0; +#else + bool isTTY = isatty(STDOUT_FILENO) != 0; +#endif + if (!isTTY) return; + bool color = shouldUseColor(false); + if (!color) return; + // Trim and limit to single-line preview (80 cols) for non-intrusive echo + std::string preview = code; + // Remove trailing newlines/spaces + while (!preview.empty() && (preview.back()=='\n' || preview.back()=='\r' || preview.back()==' ' || preview.back()=='\t')) preview.pop_back(); + // Collapse internal newlines to " ⏎ " for preview + for (char &c : preview) if (c=='\n' || c=='\r') c=' '; + // Trim leading spaces + size_t s = preview.find_first_not_of(" \t"); + if (s!=std::string::npos) preview = preview.substr(s); + 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; } bool Session::isIncomplete(const std::string &buffer) const { @@ -56,7 +223,92 @@ bool Session::isIncomplete(const std::string &buffer) const { else if (c == ']') --brackets; } if (inDouble || inSingle || inBlockComment) return true; - return braces > 0 || parens > 0 || brackets > 0; + 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; } bool Session::handleCommand(const std::string &line, std::string &err) { @@ -84,22 +336,66 @@ bool Session::handleCommand(const std::string &line, std::string &err) { interp_.reset(err); if (!err.empty()) std::cerr << "reset error: " << err << "\n"; - else + else { std::cout << "[reset]\n"; + promptCount_ = 1; + hasLastTiming_ = false; + } return true; } if (t == ":clear" || t == ":cls" || t == ":c" || t == "clear" || t == "cls") { - // Clear output buffer / terminal screen - // ANSI clear screen + move cursor home - std::cout << "\033[2J\033[H" << std::flush; #ifdef HAS_READLINE - if (isatty(STDOUT_FILENO)) { - // readline helper to clear screen and redisplay +#ifdef _WIN32 + if (_isatty(_fileno(stdin)) || _isatty(_fileno(stdout))) { +#else + if (isatty(STDOUT_FILENO) || isatty(STDIN_FILENO)) { +#endif rl_clear_screen(0, 0); rl_on_new_line(); } #endif - // Also clear any partially accumulated multiline buffer +#ifdef _WIN32 + { + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut != INVALID_HANDLE_VALUE && hOut != nullptr) { + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (GetConsoleScreenBufferInfo(hOut, &csbi)) { + COORD topLeft = {0, 0}; + DWORD written = 0; + DWORD cells = static_cast(csbi.dwSize.X) * static_cast(csbi.dwSize.Y); + FillConsoleOutputCharacterA(hOut, ' ', cells, topLeft, &written); + FillConsoleOutputAttribute(hOut, csbi.wAttributes, cells, topLeft, &written); + SetConsoleCursorPosition(hOut, topLeft); + } else { + if (std::system("cls") != 0) { + std::cout << "\033[2J\033[3J\033[H" << std::flush; + } + } + } else { + if (std::system("cls") != 0) { + std::cout << "\033[2J\033[3J\033[H" << std::flush; + } + } + } +#else + { + bool isTTY = isatty(STDOUT_FILENO) || isatty(STDERR_FILENO) || isatty(STDIN_FILENO); + if (!isTTY) { + std::cout << std::string(100, '\n') << std::flush; + } else { + const char *term = std::getenv("TERM"); + std::string termStr = term ? term : ""; + if (termStr.empty() || termStr == "dumb") { + std::cout << std::string(100, '\n') << std::flush; + } else { + int rc = std::system("clear"); + if (rc != 0) { + std::cout << "\033[2J\033[3J\033[H" << std::flush; + } + } + } + } +#endif buffer_.clear(); return true; } @@ -122,7 +418,6 @@ bool Session::handleCommand(const std::string &line, std::string &err) { std::cout << "usage: :lib (absolute, relative, or -l name)\n"; else { std::string e; - // Use addLibrary for bare names (searches -L paths), else loadLibrary for direct path bool ok = false; if (path.find('/') != std::string::npos || path.find(".so") != std::string::npos) { ok = interp_.loadLibrary(path, e); @@ -149,17 +444,19 @@ bool Session::handleCommand(const std::string &line, std::string &err) { std::string e; if (!interp_.undo(n, e)) std::cerr << "undo error: " << e << "\n"; - else + else { std::cout << "[undid " << n << "]\n"; + if (promptCount_ > (int)n + 1) promptCount_ -= n; + else promptCount_ = 1; + hasLastTiming_ = false; + } return true; } - // Include path handling (absolute & relative) – interactive :I / :include if (t.rfind(":I", 0) == 0 || t.rfind(":include", 0) == 0 || t.rfind(":inc", 0) == 0) { std::string path; if (t.rfind(":I", 0) == 0) path = trim(t.substr(2)); else if (t.rfind(":include", 0) == 0) path = trim(t.substr(8)); else path = trim(t.substr(4)); - // Support ":I=path" or ":I path" if (!path.empty() && path[0] == '=') path = trim(path.substr(1)); if (path.empty()) { std::cout << "usage: :I (add include path, absolute or relative)\n"; return true; } std::string e; @@ -194,6 +491,14 @@ void Session::runInteractive() { return s.substr(a, b - a + 1); }; + 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"; + }; + #ifdef HAS_READLINE bool useReadline = isatty(STDIN_FILENO); if (useReadline) { @@ -204,8 +509,9 @@ void Session::runInteractive() { if (!histFile.empty()) read_history(histFile.c_str()); char *raw = nullptr; while (true) { - const char *prompt = buffer_.empty() ? "cpp> " : "...> "; - raw = readline(prompt); + std::string promptStr = buffer_.empty() ? buildPrimaryPrompt(true) + : buildContinuationPrompt(true); + raw = readline(promptStr.c_str()); if (!raw) { std::cout << "\nbye\n"; break; @@ -231,13 +537,6 @@ void Session::runInteractive() { continue; } } - if (!line.empty()) { - HIST_ENTRY *last = history_get(history_length); - if (!last || std::string(last->line) != line) { - add_history(line.c_str()); - if (!histFile.empty()) append_history(1, histFile.c_str()); - } - } buffer_ += line + "\n"; bool incomplete = false; if (!line.empty() && line.back() == '\\') @@ -247,11 +546,36 @@ void Session::runInteractive() { if (incomplete) { continue; } + { + std::string histEntry = buffer_; + while (!histEntry.empty() && + (histEntry.back() == '\n' || histEntry.back() == '\r')) + histEntry.pop_back(); + if (!histEntry.empty()) { + HIST_ENTRY *last = history_get(history_length); + if (!last || histEntry != last->line) { + add_history(histEntry.c_str()); + if (!histFile.empty()) append_history(1, histFile.c_str()); + } + } + } std::string err; - if (!interp_.eval(buffer_, err)) { - if (!err.empty()) - std::cerr << "error: " << err << "\n"; + auto t0 = std::chrono::steady_clock::now(); + bool ok = interp_.eval(buffer_, err); + auto t1 = std::chrono::steady_clock::now(); + double ms = std::chrono::duration(t1 - t0).count(); + lastDurationMs_ = ms; + lastSuccess_ = ok; + hasLastTiming_ = true; + // Keyword highlight: echo executed code with syntax colors (after execution) + printHighlightedEcho(buffer_); + if (!ok) { + if (!err.empty()) printError(err); + printTimingLine(false, ms); + } else { + printTimingLine(true, ms); } + ++promptCount_; buffer_.clear(); } if (!histFile.empty()) write_history(histFile.c_str()); @@ -259,7 +583,7 @@ void Session::runInteractive() { } #endif std::string line; - std::cout << "cpp> " << std::flush; + std::cout << buildPrimaryPrompt(false) << std::flush; while (std::getline(std::cin, line)) { std::string t = trim(line); if (buffer_.empty()) { @@ -267,12 +591,12 @@ void Session::runInteractive() { t == "exit" || t == "quit") break; if (t.empty()) { - std::cout << "cpp> " << std::flush; + std::cout << buildPrimaryPrompt(false) << std::flush; continue; } std::string err; if (handleCommand(line, err)) { - std::cout << "cpp> " << std::flush; + std::cout << buildPrimaryPrompt(false) << std::flush; continue; } } @@ -283,16 +607,27 @@ void Session::runInteractive() { else if (isIncomplete(buffer_)) incomplete = true; if (incomplete) { - std::cout << "...> " << std::flush; + std::cout << buildContinuationPrompt(false) << std::flush; continue; } std::string err; - if (!interp_.eval(buffer_, err)) { - if (!err.empty()) - std::cerr << "error: " << err << "\n"; + auto t0 = std::chrono::steady_clock::now(); + bool ok = interp_.eval(buffer_, err); + auto t1 = std::chrono::steady_clock::now(); + double ms = std::chrono::duration(t1 - t0).count(); + lastDurationMs_ = ms; + lastSuccess_ = ok; + hasLastTiming_ = true; + printHighlightedEcho(buffer_); + if (!ok) { + if (!err.empty()) printError(err); + printTimingLine(false, ms); + } else { + printTimingLine(true, ms); } + ++promptCount_; buffer_.clear(); - std::cout << "cpp> " << std::flush; + std::cout << buildPrimaryPrompt(false) << std::flush; } std::cout << "\nbye\n"; } diff --git a/src/utils/bigint.cpp b/src/utils/bigint.cpp index cb8ef7a..5eb2528 100644 --- a/src/utils/bigint.cpp +++ b/src/utils/bigint.cpp @@ -1,3 +1,7 @@ +/** + * @file bigint.cpp + * @brief BigInt preamble definitions. + */ #include "cpp-repl/utils/bigint.h" namespace cpprepl { diff --git a/src/utils/highlight.cpp b/src/utils/highlight.cpp new file mode 100644 index 0000000..c9d627e --- /dev/null +++ b/src/utils/highlight.cpp @@ -0,0 +1,135 @@ +#include "cpp-repl/utils/highlight.h" +#include +#include + +namespace cpprepl { +namespace utils { + +static const std::unordered_set kKeywords = { + "alignas","alignof","and","and_eq","asm","auto","bitand","bitor","bool","break","case","catch","char","char8_t","char16_t","char32_t","class","compl","concept","const","consteval","constexpr","constinit","const_cast","continue","co_await","co_return","co_yield","decltype","default","delete","do","double","dynamic_cast","else","enum","explicit","export","extern","false","float","for","friend","goto","if","inline","int","long","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","private","protected","public","register","reinterpret_cast","requires","return","short","signed","sizeof","static","static_assert","static_cast","struct","switch","template","this","thread_local","throw","true","try","typedef","typeid","typename","union","unsigned","using","virtual","void","volatile","wchar_t","while","xor","xor_eq","import","module" +}; + +static const std::unordered_set kTypes = { + "int","float","double","char","bool","void","long","short","unsigned","signed","size_t","auto","std::string","string","std::vector","std::map","cpp_int","bigint","mpz_int","int64_t","uint64_t" +}; + +static const std::unordered_set kConsts = {"true","false","nullptr"}; + +std::string Highlighter::highlight(const std::string &code, bool useColor) { + if (!useColor || code.empty()) return code; + const std::string RST = "\033[0m"; + const std::string GREY = "\033[90m"; + const std::string YELLOW = "\033[33m"; + const std::string MAGENTA = "\033[95m"; + const std::string CYAN = "\033[36m"; + const std::string B_CYAN = "\033[1;36m"; + const std::string GREEN = "\033[32m"; + const std::string B_MAGENTA = "\033[1;35m"; + const std::string DIM = "\033[2m"; + // Preprocessor yellow bright + std::string out; + out.reserve(code.size()*2); + size_t i=0, n=code.size(); + bool atLineStart = true; + while (i diff --git a/src/vm.cpp b/src/vm.cpp index 13247ae..b0b0b77 100644 --- a/src/vm.cpp +++ b/src/vm.cpp @@ -1,3 +1,7 @@ +/** + * @file vm.cpp + * @brief Legacy VM wrapper for llvm::orc::LLJIT. + */ #include "vm.h" #include "llvm/ExecutionEngine/Orc/ThreadSafeModule.h" diff --git a/src/vm.h b/src/vm.h index 0b663a5..7b0619f 100644 --- a/src/vm.h +++ b/src/vm.h @@ -1,3 +1,7 @@ +/** + * @file vm.h + * @brief Legacy low-level VM wrapper. + */ #ifndef CPP_REPL_VM_H #define CPP_REPL_VM_H @@ -10,9 +14,12 @@ namespace vm { -/// Low-level VM wrapping llvm::orc::LLJIT. -/// No optimizations – O0, pure execution. -/// This is the raw execution layer; C++ REPL builds on top. +/** + * @brief Low-level VM wrapping llvm::orc::LLJIT. + * + * No optimizations, O0, pure execution. + * Raw execution layer that the C++ REPL builds on. + */ class VM { public: VM(); @@ -21,18 +28,22 @@ class VM { VM(const VM &) = delete; VM &operator=(const VM &) = delete; - // Initialize JIT – must be called once. + /** @brief Initialize JIT, must be called once. */ bool init(std::string &err); - // Add a Module (takes ownership). Module is moved into ThreadSafeModule. + /** + * @brief Add a module to the JIT. + * @param M Module to move. + * @param Ctx Context for the module. + * @param err Output error. + * @return true on success. + */ bool addModule(std::unique_ptr M, std::unique_ptr Ctx, std::string &err); - // Lookup symbol address (mangled IR name) + /** @brief Lookup symbol address by IR name. */ llvm::Expected lookup(const std::string &name); - // For debugging: dump current state not needed at this level - llvm::orc::LLJIT *getLLJIT() { return jit_.get(); } private: