diff --git a/.clang-tidy b/.clang-tidy index 219fc86..64b029b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,4 +1,7 @@ --- + +# Unfortunately const-correctness seems to be almost completely broken +# (clang 19) Checks: "*, -abseil-*, -altera-*, @@ -11,18 +14,29 @@ Checks: "*, -readability-else-after-return, -readability-static-accessed-through-instance, -readability-avoid-const-params-in-decls, + -readability-simplify-boolean-expr, -cppcoreguidelines-non-private-member-variables-in-classes, -misc-non-private-member-variables-in-classes, + -misc-const-correctness, " WarningsAsErrors: '' -HeaderFilterRegex: '' +HeaderFilterRegex: '^(src|include)/.*' FormatStyle: none -CheckOptions: - readability-identifier-length.IgnoredVariableNames: 'x|y|z|id|ch' - readability-identifier-length.IgnoredParameterNames: 'x|y|z|id|ch' - +# Command line options +ExtraArgs: [ + '-Wno-unknown-warning-option', + '-Wno-ignored-optimization-argument', + '-Wno-unused-command-line-argument', + '-Wno-unknown-argument', + '-Wno-gcc-compat' +] +# Quiet mode is set via command line with --quiet +# It doesn't have a YAML equivalent +CheckOptions: + readability-identifier-length.IgnoredVariableNames: 'x|y|z|id|ch|to' + readability-identifier-length.IgnoredParameterNames: 'x|y|z|id|ch|to' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef78340..7e57f35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: - develop env: - CLANG_TIDY_VERSION: "16.0.0" + CLANG_TIDY_VERSION: "19.1.1" VERBOSE: 1 @@ -29,10 +29,11 @@ jobs: # and your own projects needs matrix: os: - - ubuntu-22.04 + - ubuntu-latest compiler: # you can specify the version after `-` like "llvm-16.0.0". - - gcc-13 + - gcc-14 + - llvm-19.1.1 generator: - "Ninja Multi-Config" build_type: @@ -45,8 +46,8 @@ jobs: include: # Add appropriate variables for gcov version required. This will intentionally break # if you try to use a compiler that does not have gcov set - - compiler: gcc-13 - gcov_executable: gcov + - compiler: gcc-14 + gcov_executable: gcov-14 enable_ipo: On diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 0000000..83aa49c --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,59 @@ +name: Build Intro WASM and Deploy to GitHub Pages + +on: + pull_request: + release: + types: [published] + push: + branches: [main, develop] + tags: ['**'] + +permissions: + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: 'latest' + + - name: Install Ninja + run: sudo apt-get install -y ninja-build + + - name: Configure CMake + run: emcmake cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build all WASM targets + run: emmake cmake --build build --target web-dist + + - name: Prepare deployment + run: | + # web-dist target already created build/web-dist/ + # Just copy it to dist/ for GitHub Pages action + cp -r build/web-dist dist + + - name: Determine deploy path + id: deploy-path + if: github.event_name != 'pull_request' && github.event_name != 'release' + run: | + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + echo "path=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == refs/heads/main ]]; then + echo "path=." >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == refs/heads/develop ]]; then + echo "path=develop" >> $GITHUB_OUTPUT + fi + + - name: Deploy to GitHub Pages + if: github.event_name != 'pull_request' && github.event_name != 'release' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + destination_dir: ${{ steps.deploy-path.outputs.path }} + keep_files: true diff --git a/.gitignore b/.gitignore index a3f1df0..6ff6f6a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ $RECYCLE.BIN/ .TemporaryItems ehthumbs.db Thumbs.db + +**/.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md index 92faa82..b29e5de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +You are an expert in C++. You use C++23 and prefer to use constexpr wherever possible. You always apply C++ Best Practices as taught by Jason Turner. + +You are also an expert in scheme-like languages and know the pros and cons of various design decisions. + + + ## Build Commands - Configure: `cmake -S . -B ./build` - Build: `cmake --build ./build` @@ -16,6 +22,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `constexpr_tests` target compiles tests with static assertions - Will fail to compile if tests fail since they use static assertions - Makes debugging difficult as you won't see which specific test failed + - Will always fail to compile if there's a fail test; use relaxed_constexpr_tests or directly execute the tests with cons_expr command line tool for debugging - `relaxed_constexpr_tests` target compiles with runtime assertions - Preferred for debugging since it shows which specific tests fail - Use this target when developing/debugging: @@ -57,6 +64,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Header files follow #ifndef/#define guard pattern - Entire system is `constexpr` capable unless it uses IO - Use modern C++ style casts over C-style casts +- Avoid macros completely except for header guards +- Prefer templates, constexpr functions or concepts over macros +- Use `static constexpr` for compile-time known constants +- Prefer local constants within functions over function variables for readability ## Naming and Structure - Namespace: lefticus diff --git a/CMakeLists.txt b/CMakeLists.txt index fd6b7cb..19fde27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,8 +23,19 @@ project( LANGUAGES CXX C) include(cmake/PreventInSourceBuilds.cmake) +include(cmake/Emscripten.cmake) include(ProjectOptions.cmake) +if(MSVC) + +elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + add_compile_options(-fconstexpr-steps=12712420) +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + +else() + + # TODO support Intel compiler +endif() cons_expr_setup_options() @@ -67,6 +78,11 @@ add_subdirectory(src) add_subdirectory(examples) +# Create unified web deployment directory (for WASM builds) +if(EMSCRIPTEN) + cons_expr_create_web_dist() +endif() + # Don't even look at tests if we're not top level if(NOT PROJECT_IS_TOP_LEVEL) return() diff --git a/Dependencies.cmake b/Dependencies.cmake index 466ff07..a9c8c31 100644 --- a/Dependencies.cmake +++ b/Dependencies.cmake @@ -21,7 +21,7 @@ function(cons_expr_setup_dependencies) endif() if(NOT TARGET Catch2::Catch2WithMain) - cpmaddpackage("gh:catchorg/Catch2@3.7.0") + cpmaddpackage("gh:catchorg/Catch2@3.8.1") endif() if(NOT TARGET CLI11::CLI11) diff --git a/ProjectOptions.cmake b/ProjectOptions.cmake index 26884a0..b5b4222 100644 --- a/ProjectOptions.cmake +++ b/ProjectOptions.cmake @@ -4,9 +4,31 @@ include(CMakeDependentOption) include(CheckCXXCompilerFlag) +include(CheckCXXSourceCompiles) + + macro(cons_expr_supports_sanitizers) - if((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32) - set(SUPPORTS_UBSAN ON) + # Emscripten doesn't support sanitizers + if(EMSCRIPTEN) + set(SUPPORTS_UBSAN OFF) + set(SUPPORTS_ASAN OFF) + elseif((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32) + + message(STATUS "Sanity checking UndefinedBehaviorSanitizer, it should be supported on this platform") + set(TEST_PROGRAM "int main() { return 0; }") + + # Check if UndefinedBehaviorSanitizer works at link time + set(CMAKE_REQUIRED_FLAGS "-fsanitize=undefined") + set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=undefined") + check_cxx_source_compiles("${TEST_PROGRAM}" HAS_UBSAN_LINK_SUPPORT) + + if(HAS_UBSAN_LINK_SUPPORT) + message(STATUS "UndefinedBehaviorSanitizer is supported at both compile and link time.") + set(SUPPORTS_UBSAN ON) + else() + message(WARNING "UndefinedBehaviorSanitizer is NOT supported at link time.") + set(SUPPORTS_UBSAN OFF) + endif() else() set(SUPPORTS_UBSAN OFF) endif() @@ -14,7 +36,25 @@ macro(cons_expr_supports_sanitizers) if((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND WIN32) set(SUPPORTS_ASAN OFF) else() - set(SUPPORTS_ASAN ON) + if (NOT WIN32) + message(STATUS "Sanity checking AddressSanitizer, it should be supported on this platform") + set(TEST_PROGRAM "int main() { return 0; }") + + # Check if AddressSanitizer works at link time + set(CMAKE_REQUIRED_FLAGS "-fsanitize=address") + set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=address") + check_cxx_source_compiles("${TEST_PROGRAM}" HAS_ASAN_LINK_SUPPORT) + + if(HAS_ASAN_LINK_SUPPORT) + message(STATUS "AddressSanitizer is supported at both compile and link time.") + set(SUPPORTS_ASAN ON) + else() + message(WARNING "AddressSanitizer is NOT supported at link time.") + set(SUPPORTS_ASAN OFF) + endif() + else() + set(SUPPORTS_ASAN ON) + endif() endif() endmacro() @@ -54,8 +94,8 @@ macro(cons_expr_setup_options) option(cons_expr_ENABLE_SANITIZER_THREAD "Enable thread sanitizer" OFF) option(cons_expr_ENABLE_SANITIZER_MEMORY "Enable memory sanitizer" OFF) option(cons_expr_ENABLE_UNITY_BUILD "Enable unity builds" OFF) - option(cons_expr_ENABLE_CLANG_TIDY "Enable clang-tidy" OFF) - option(cons_expr_ENABLE_CPPCHECK "Enable cpp-check analysis" OFF) + option(cons_expr_ENABLE_CLANG_TIDY "Enable clang-tidy" ON) + option(cons_expr_ENABLE_CPPCHECK "Enable cpp-check analysis" ON) option(cons_expr_ENABLE_PCH "Enable precompiled headers" OFF) option(cons_expr_ENABLE_CACHE "Enable ccache" ON) endif() @@ -130,19 +170,22 @@ macro(cons_expr_local_options) "" "") - if(cons_expr_ENABLE_USER_LINKER) - include(cmake/Linker.cmake) - configure_linker(cons_expr_options) - endif() + # Linker and sanitizers not supported in Emscripten + if(NOT EMSCRIPTEN) + if(cons_expr_ENABLE_USER_LINKER) + include(cmake/Linker.cmake) + cons_expr_configure_linker(cons_expr_options) + endif() - include(cmake/Sanitizers.cmake) - cons_expr_enable_sanitizers( - cons_expr_options - ${cons_expr_ENABLE_SANITIZER_ADDRESS} - ${cons_expr_ENABLE_SANITIZER_LEAK} - ${cons_expr_ENABLE_SANITIZER_UNDEFINED} - ${cons_expr_ENABLE_SANITIZER_THREAD} - ${cons_expr_ENABLE_SANITIZER_MEMORY}) + include(cmake/Sanitizers.cmake) + cons_expr_enable_sanitizers( + cons_expr_options + ${cons_expr_ENABLE_SANITIZER_ADDRESS} + ${cons_expr_ENABLE_SANITIZER_LEAK} + ${cons_expr_ENABLE_SANITIZER_UNDEFINED} + ${cons_expr_ENABLE_SANITIZER_THREAD} + ${cons_expr_ENABLE_SANITIZER_MEMORY}) + endif() set_target_properties(cons_expr_options PROPERTIES UNITY_BUILD ${cons_expr_ENABLE_UNITY_BUILD}) diff --git a/README.md b/README.md index dd702a1..d52c11d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,15 @@ * For C++23 * Currently only known to work with GCC 13.1. +* WebAssembly build support with automatic GitHub Pages deployment +**Live Demo:** If you enable GitHub Pages in your project created from this template, you'll have a working example like this: + +- Main: [https://cpp-best-practices.github.io/cmake_template/](https://cpp-best-practices.github.io/cmake_template/) +- Develop: [https://cpp-best-practices.github.io/cmake_template/develop/](https://cpp-best-practices.github.io/cmake_template/develop/) + +The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`. + ## Command Line Inspection Tool `ccons_expr` can be used to execute scripts and inspect the state of the runtime system live diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..eb1cd33 --- /dev/null +++ b/TODO.md @@ -0,0 +1,1163 @@ +# cons_expr TODOs + +A prioritized list of features for making cons_expr a practical embedded Scheme-like language for C++ integration. + +## Critical (Safety & Correctness) + +- [ ] **Optional Safe Numeric Types** + - Create a new header file `numerics.hpp` with optional safe numeric types + - Implement `Rational` type for exact fraction arithmetic as a replacement for `int_type` + - Implement `Safe` template for both integral and floating point types with checked operations + - Allow users to choose these types to enhance safety and exactness: + - `Rational` - Exact fraction arithmetic for integer operations + - `Safe` - Error checking wrapper for any numeric type (int or float) + - Keep default numeric operations as-is (division by zero will signal/crash) + - Follow "pay for what you use" principle - users who need safety/exactness should explicitly opt in + +- [X] **Improved Lexical Scoping** + - Fix variable capture in closures + - Fix scoping issues in lambdas + - Essential for predictable behavior + +- [ ] **Memory Usage Optimizer (Compaction)** + - Implement non-member "compact" function in utility.hpp as an opt-in feature + - Use two-phase mark-and-compact approach: + 1. Mark Phase: Identify all reachable elements from global_scope + 2. Compact Phase: Create new containers and remap indices + - Critical for long-running embedded scripts with memory constraints + - Allows reclaiming space from unreachable values in fixed-size containers + - Avoid in-place compaction which is more complex and error-prone + +- [ ] **Better Error Propagation** + - Ensure errors bubble up properly to C++ caller + - Add context about what went wrong + - Allow C++ code to catch and handle script errors gracefully + - Implement container capacity error detection and reporting: + 1. Add detection functions to identify when SmallVector containers enter error state + 2. Propagate container errors during evaluation and parsing + 3. Create specific error types for container overflow errors + 4. Ensure container errors are reported with container-specific context + 5. Add tests to verify correct error reporting for container capacity issues + +## High Priority (Core Functionality) + +- [ ] **C++ ↔ Script Data Exchange** + - Expand the existing function call mechanism with container support + - Add automatic conversion between Scheme lists and C++ containers: + - std::vector ↔ Scheme lists + - std::map/std::unordered_map ↔ Scheme association lists + - std::tuple ↔ Scheme lists of fixed size + - Add constexpr tests for C++ ↔ Scheme function calls + - Example goal: `auto result = evaluator.call("my-function", 10, "string", std::vector{1,2,3})` + +- [X] **Basic Type Predicates** + - Core set: `number?`, `string?`, `list?`, `procedure?`, etc. + - Implemented with a flexible variadic template approach + - Essential for type checking within scripts + - Allows scripts to handle mixed-type data from C++ + +- [ ] **List Utilities** + - `length` - Count elements in a list + - `map` - Transform lists (basic functional building block) + - `filter` - Filter lists based on predicate + - `foldl`/`foldr` - Reduce a list to a single value (sum, product, etc.) + - `reverse` - Reverse a list + - `member` - Check if an element is in a list + - `assoc` - Look up key-value pairs in an association list + - These operations are fundamental and tedious to implement in scripts + - Implementation should follow functional programming patterns with immutability + +- [ ] **Transparent C++ Function Registration** + - Build on existing template function registration + - Add support for lambdas and function objects with deduced types + - Example: `evaluator.register_function("add", [](int a, int b) { return a + b; })` + - Implement converters for more complex C++ types: + - Support for std::optional return values + - Support for std::expected return values for error handling + - Support for user-defined types with conversion traits + - Create a cleaner API that maintains type safety but reduces template verbosity + +## Medium Priority (Usability & Performance) + +- [ ] **Add `letrec` Support** + - Support recursive bindings in `let` expressions + - Support mutual recursion without forward declarations + - Follow standard Scheme semantics for `letrec` + - Implementation approach: + - Build on existing self-referential closure mechanism + - Create a new scope where all variables are pre-defined (but uninitialized) + - Evaluate right-hand sides in that scope + - Bind results to the pre-defined variables + - Syntax: `(letrec ((name1 value1) (name2 value2) ...) body ...)` + - This complements the current `let` which uses sequential binding + +- [ ] **Constant Folding** + - Optimize expressions that can be evaluated at compile time + - Performance boost for embedded use + - Makes constexpr evaluation more efficient + - Implementation strategy: + - Add a "pure" flag to function pointers that guarantees no side effects + - During parsing phase, identify expressions with only pure operations + - Pre-evaluate these expressions and replace with their result + - Add caching for common constant expressions + - Implementation should preserve semantics exactly + - Potential optimizations: + - Arithmetic expressions with constant operands: `(+ 1 2 3)` → `6` + - Constant string operations: `(string-append "hello" " " "world")` → `"hello world"` + - Pure function calls with constant arguments + - Condition expressions with constant predicates: `(if true x y)` → `x` + +- [ ] **Basic Math Functions** + - Minimal set: `abs`, `min`, `max` + - Common operations that C++ code might expect + +- [ ] **Vector Support** + - Random access data structure + - More natural for interfacing with C++ std::vector + - Useful for passing arrays of data between C++ and script + +- [ ] **Script Function Memoization** + - Cache results of pure functions + - Performance optimization for embedded use + - Example: `(define-memoized fibonacci (lambda (n) ...))` + +- [ ] **Script Interrupt/Timeout** + - Allow C++ to interrupt long-running scripts + - Set execution time limits + - Essential for embedded use where scripts shouldn't block main application + +## Optional Enhancements + +- [ ] **Debugging Support** + - Script debugging facilities + - Integration with C++ debugging tools + - Breakpoints, variable inspection + - Makes embedded scripts easier to maintain + +- [ ] **Profiling Tools** + - Measure script performance + - Identify hotspots for optimization + - Useful for optimizing embedded scripts + +- [ ] **Sandbox Mode** + - Restrict which functions a script can access + - Limit resource usage + - Important for security in embedded contexts + +- [ ] **Script Hot Reloading** + - Update scripts without restarting application + - Useful for development and game scripting + +- [ ] **Incremental GC** + - Non-blocking memory management + - Important for real-time applications + +## Implementation Notes + +1. **Comparison with Other Embedded Schemes**: + - Unlike Guile/Chicken: Focus on C++23 integration over standalone usage + - Unlike TinyScheme: Prioritize constexpr/compile-time evaluation + - Like ChaiScript: Emphasize tight C++ integration, but with Scheme syntax + +2. **Key Differentiation**: + - Compile-time script evaluation via constexpr + - No dynamic allocation requirement + - C++23 features for cleaner integration + - Fixed buffer sizes for embedded environments + +3. **Design Philosophy**: + - Favor predictable performance over language completeness + - Favor C++ compatibility over Scheme compatibility + - Treat scripts as extensions of C++, not standalone programs + +4. **Use Cases to Consider**: + - Game scripting (behaviors, AI) + - Configuration (loading settings) + - Rule engines (business logic) + - UI event handling + - Embedded device scripting + +5. **C++ Integration Best Practices**: + - Use strong typing when passing data between C++ and script + - Keep scripts focused on high-level logic + - Implement performance-critical code in C++ + - Use scripts for parts that need runtime modification + +6. **Safe Numerics Implementation Plan**: + - **Design Goals**: + - Provide optional numeric types with guaranteed safety + - Make them drop-in replacements for standard numeric types + - Support both C++ and Scheme semantics + - Maintain constexpr compatibility + + - **Components**: + 1. **Rational**: + - Exact representation of fractions (e.g., 1/3) without rounding errors + - Replace int_type for exact arithmetic + - Store as numerator/denominator pair of BaseType + - Support all basic operations while preserving exactness + - Detect division by zero and handle gracefully + - Optional normalization (dividing by GCD) + - Example: + ```cpp + template + struct Rational { + BaseType numerator; + BaseType denominator; // never zero + + // Various arithmetic operations... + constexpr Rational operator+(const Rational& other) const; + constexpr Rational operator/(const Rational& other) const { + if (other.numerator == 0) { + // Handle division by zero - could set error flag or return NaN equivalent + } + return Rational{numerator * other.denominator, denominator * other.numerator}; + } + }; + ``` + + 2. **Safe**: + - Wrapper around any numeric type with error checking + - Can be used for both int_type and real_type + - Detect overflow, underflow, division by zero + - Hold error state internally + - Example: + ```cpp + template + struct Safe { + T value; + bool error_state = false; + + constexpr Safe operator/(const Safe& other) const { + if (other.value == 0) { + return Safe{0, true}; // Error state true + } + return Safe{value / other.value}; + } + }; + ``` + + - **Integration Strategy**: + ```cpp + // Example usage in cons_expr instances: + + // Use Rational for exact arithmetic with fractions + using ExactEval = lefticus::cons_expr< + std::uint16_t, + char, + lefticus::Rational, // Replace int_type with Rational + double // Keep regular floating point + >; + + // Use Safe wrappers for error detection + using SafeEval = lefticus::cons_expr< + std::uint16_t, + char, + lefticus::Safe, // Safe integer operations + lefticus::Safe // Safe floating point operations + >; + + // Combine both approaches + using SafeExactEval = lefticus::cons_expr< + std::uint16_t, + char, + lefticus::Safe>, // Safe exact arithmetic + lefticus::Safe // Safe floating point + >; + ``` + +7. **List Utilities Implementation Plan**: + - **Design Goals**: + - Provide standard functional list operations + - Maintain immutability of data + - Support both literal_list_type and list_type where appropriate + - Follow Scheme/LISP conventions + - Maximize constexpr compatibility + + - **Core Functions**: + 1. **length**: + ```cpp + // Basic list length calculation + [[nodiscard]] static constexpr SExpr length(cons_expr &engine, LexicalScope &scope, list_type params) + { + if (params.size != 1) { return engine.make_error(str("(length list)"), params); } + + const auto list_result = engine.eval_to(scope, engine.values[params[0]]); + if (!list_result) { return engine.make_error(str("expected list"), list_result.error()); } + + return SExpr{ Atom(static_cast(list_result->items.size)) }; + } + ``` + + 2. **map**: + ```cpp + // Transform a list by applying a function to each element + [[nodiscard]] static constexpr SExpr map(cons_expr &engine, LexicalScope &scope, list_type params) + { + if (params.size != 2) { return engine.make_error(str("(map function list)"), params); } + + const auto func = engine.eval(scope, engine.values[params[0]]); + const auto list_result = engine.eval_to(scope, engine.values[params[1]]); + + if (!list_result) { return engine.make_error(str("expected list"), list_result.error()); } + + // Create a new list with the results of applying the function to each element + Scratch result{ engine.object_scratch }; + + for (const auto &item : engine.values[list_result->items]) { + // Apply function to each item + std::array args{ item }; + const auto mapped_item = engine.invoke_function(scope, func, engine.values.insert_or_find(args)); + + // Check for container errors after each operation + if (engine.has_container_error()) { + return engine.make_container_error(); + } + + result.push_back(mapped_item); + } + + return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; + } + ``` + + 3. **filter**: + ```cpp + // Filter a list based on a predicate function + [[nodiscard]] static constexpr SExpr filter(cons_expr &engine, LexicalScope &scope, list_type params) + { + if (params.size != 2) { return engine.make_error(str("(filter predicate list)"), params); } + + const auto pred = engine.eval(scope, engine.values[params[0]]); + const auto list_result = engine.eval_to(scope, engine.values[params[1]]); + + if (!list_result) { return engine.make_error(str("expected list"), list_result.error()); } + + // Create a new list with only elements that satisfy the predicate + Scratch result{ engine.object_scratch }; + + for (const auto &item : engine.values[list_result->items]) { + // Apply predicate to each item + std::array args{ item }; + const auto pred_result = engine.invoke_function(scope, pred, engine.values.insert_or_find(args)); + + // Check if predicate returned true + const auto bool_result = engine.eval_to(scope, pred_result); + if (!bool_result) { + return engine.make_error(str("predicate must return boolean"), pred_result); + } + + // Add item to result if predicate is true + if (*bool_result) { + result.push_back(item); + } + } + + return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; + } + ``` + + - **Additional Functions**: + - `foldl`/`foldr` for reduction operations + - `reverse` for creating a reversed copy of a list + - `member` for checking list membership + - `assoc` for working with association lists (key-value pairs) + + - **Registration**: + ```cpp + // Add to consteval cons_expr() constructor + add(str("length"), SExpr{ FunctionPtr{ length, FunctionPtr::Type::other } }); + add(str("map"), SExpr{ FunctionPtr{ map, FunctionPtr::Type::other } }); + add(str("filter"), SExpr{ FunctionPtr{ filter, FunctionPtr::Type::other } }); + // Add other list utility functions... + ``` + +8. **Memory Compaction Implementation Plan**: + - **Design Goals**: + - Create a non-member utility function for memory compaction + - Safely reduce memory usage by removing unreachable items + - Preserve all reachable values with correct indexing + - Support constexpr operation + - Zero dynamic allocation + + - **Implementation Strategy**: + ```cpp + // Non-member compact function in utility.hpp + template + constexpr void compact(Eval& evaluator) { + using size_type = typename Eval::size_type; + + // Phase 1: Mark all reachable elements + std::array string_reachable{}; + std::array value_reachable{}; + + // Start from global scope and recursively mark everything reachable + for (const auto& [name, value] : evaluator.global_scope) { + mark_reachable_string(name, string_reachable, evaluator); + mark_reachable_value(value, string_reachable, value_reachable, evaluator); + } + + // Phase 2: Build index mapping tables + std::array string_index_map{}; + std::array value_index_map{}; + + size_type new_string_idx = 0; + for (size_type i = 0; i < evaluator.strings.small_size_used; ++i) { + if (string_reachable[i]) { + string_index_map[i] = new_string_idx++; + } + } + + size_type new_value_idx = 0; + for (size_type i = 0; i < evaluator.values.small_size_used; ++i) { + if (value_reachable[i]) { + value_index_map[i] = new_value_idx++; + } + } + + // Phase 3: Create new containers with only reachable elements + auto new_strings = evaluator.strings; + auto new_values = evaluator.values; + auto new_global_scope = evaluator.global_scope; + + // Reset counters + new_strings.small_size_used = 0; + new_values.small_size_used = 0; + new_global_scope.small_size_used = 0; + + // Copy and remap strings + for (size_type i = 0; i < evaluator.strings.small_size_used; ++i) { + if (string_reachable[i]) { + new_strings.small[string_index_map[i]] = evaluator.strings.small[i]; + new_strings.small_size_used++; + } + } + + // Copy and remap values (recursively update all indices) + for (size_type i = 0; i < evaluator.values.small_size_used; ++i) { + if (value_reachable[i]) { + new_values.small[value_index_map[i]] = rewrite_indices( + evaluator.values.small[i], string_index_map, value_index_map); + new_values.small_size_used++; + } + } + + // Rebuild global scope with remapped indices + for (const auto& [name, value] : evaluator.global_scope) { + using string_type = typename Eval::string_type; + + string_type new_name{string_index_map[name.start], name.size}; + auto new_value = rewrite_indices(value, string_index_map, value_index_map); + + new_global_scope.push_back({new_name, new_value}); + } + + // Replace the old containers with the new ones + evaluator.strings = std::move(new_strings); + evaluator.values = std::move(new_values); + evaluator.global_scope = std::move(new_global_scope); + + // Reset error states that may have been set + evaluator.strings.error_state = false; + evaluator.values.error_state = false; + evaluator.global_scope.error_state = false; + } + + // Helper function to mark reachable strings + template + constexpr void mark_reachable_string( + const typename Eval::string_type& str, + std::array& string_reachable, + const Eval& evaluator) { + // Mark the string itself + string_reachable[str.start] = true; + } + + // Helper function to mark reachable values recursively + template + constexpr void mark_reachable_value( + const typename Eval::SExpr& expr, + std::array& string_reachable, + std::array& value_reachable, + const Eval& evaluator) { + + // Handle different variant types in SExpr + std::visit([&](const auto& value) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + // Handle atomic types + std::visit([&](const auto& atom) { + using AtomT = std::decay_t; + + // Mark strings in atoms + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v) { + mark_reachable_string(atom, string_reachable, evaluator); + } + // Other atom types don't contain references + }, value); + } + else if constexpr (std::is_same_v) { + // Mark all elements in the list + value_reachable[value.start] = true; + for (size_type i = 0; i < value.size; ++i) { + const auto& list_item = evaluator.values.small[value.start + i]; + mark_reachable_value(list_item, string_reachable, value_reachable, evaluator); + } + } + else if constexpr (std::is_same_v) { + // Mark all elements in the literal list + mark_reachable_value( + typename Eval::SExpr{value.items}, + string_reachable, value_reachable, evaluator); + } + else if constexpr (std::is_same_v) { + // Mark parameter names and statements + value_reachable[value.parameter_names.start] = true; + value_reachable[value.statements.start] = true; + + // Mark all parameter names + for (size_type i = 0; i < value.parameter_names.size; ++i) { + mark_reachable_value( + evaluator.values.small[value.parameter_names.start + i], + string_reachable, value_reachable, evaluator); + } + + // Mark all statements + for (size_type i = 0; i < value.statements.size; ++i) { + mark_reachable_value( + evaluator.values.small[value.statements.start + i], + string_reachable, value_reachable, evaluator); + } + + // Mark self identifier if present + if (value.has_self_reference()) { + mark_reachable_string(value.self_identifier, string_reachable, evaluator); + } + } + // Other types like FunctionPtr don't contain references to track + }, expr.value); + } + + // Helper function to recursively rewrite indices in all data structures + template + constexpr typename Eval::SExpr rewrite_indices( + const typename Eval::SExpr& expr, + const std::array& string_map, + const std::array& value_map) { + + using SExpr = typename Eval::SExpr; + + return std::visit([&](const auto& value) -> SExpr { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + // Rewrite indices in atom types if needed + return SExpr{std::visit([&](const auto& atom) { + using AtomT = std::decay_t; + + if constexpr (std::is_same_v) { + return typename Eval::Atom{typename Eval::string_type{ + string_map[atom.start], atom.size}}; + } + else if constexpr (std::is_same_v) { + return typename Eval::Atom{typename Eval::identifier_type{ + string_map[atom.start], atom.size}}; + } + else if constexpr (std::is_same_v) { + return typename Eval::Atom{typename Eval::symbol_type{ + string_map[atom.start], atom.size}}; + } + else { + // Other atoms don't need remapping + return typename Eval::Atom{atom}; + } + }, value)}; + } + else if constexpr (std::is_same_v) { + // Remap list indices + return SExpr{typename Eval::list_type{ + value_map[value.start], value.size}}; + } + else if constexpr (std::is_same_v) { + // Remap literal list indices + return SExpr{typename Eval::literal_list_type{ + typename Eval::list_type{value_map[value.items.start], value.items.size}}}; + } + else if constexpr (std::is_same_v) { + // Remap closure indices + typename Eval::Closure new_closure; + new_closure.parameter_names = { + value_map[value.parameter_names.start], value.parameter_names.size}; + new_closure.statements = { + value_map[value.statements.start], value.statements.size}; + + // Remap self identifier if present + if (value.has_self_reference()) { + new_closure.self_identifier = { + string_map[value.self_identifier.start], value.self_identifier.size}; + } + + return SExpr{new_closure}; + } + else { + // Other types like FunctionPtr don't contain indices + return SExpr{value}; + } + }, expr.value); + } + ``` + +9. **Container Error Detection Plan**: + - **Problems**: + 1. SmallVector sets error_state flags when capacity limits are exceeded, but these errors are not currently propagated or reported + 2. **Critical Issue**: SmallVector's higher-level insert methods don't check for failures: + - The base insert() sets error_state when capacity is exceeded but returns a potentially invalid index + - insert_or_find() and insert(SpanType values) call the base insert() but don't check if it succeeded + - These methods continue to use potentially invalid indices from the base insert() + - This propagates bad values into the KeyType results and makes overflow errors extremely difficult to debug + - Need to ensure these methods check error_state and handle failures appropriately + - **Root cause**: Running out of capacity in one of the fixed-size containers: + - global_scope: Fixed number of symbols/variables + - strings: Fixed space for string data + - values: Fixed number of SExpr values + - Various scratch spaces used during evaluation + - **Implementation Strategy**: + - Phase 1 - Error Detection: + - Add helper method to detect error states in all containers + - Check both global and local scope objects + - Check all containers at key points during evaluation + - Phase 2 - Error Propagation: + - Modify evaluation functions to check for errors before/after operations + - Propagate container errors to the caller via error SExpr + - Ensure error states from containers bubble up through the call stack + - Phase 3 - Error Reporting: + - Create specific error messages for different container types + - Include container size/capacity information in error messages + - Add helper to identify which specific container is in error state + - **Critical**: Handle the circular dependency where creating error strings might itself fail: + - Pre-allocate/reserve all error message strings during initialization + - Or use numeric error codes that don't require string allocation + - Or implement a fallback mechanism that avoids string allocation for error reports + - Ensure error reporting path doesn't allocate additional strings when strings container is full + - Phase 4 - Testing Plan: + 1. **Test global_scope overflow**: + - Create a test that defines variables until global_scope capacity is exceeded + - Verify correct error code/message is returned + - Check that subsequent evaluation operations fail appropriately + + 2. **Test strings table overflow**: + - Create a test that adds unique strings until strings capacity is exceeded + - Verify overflow is detected and reported correctly + - Test both direct string creation and indirect string creation (via identifiers) + + 3. **Test values table overflow**: + - Create a test with deeply nested expressions that exceed values capacity + - Create a test with many list elements that exceed values capacity + - Verify appropriate errors are generated + + 4. **Test scratch space overflows**: + - Create tests that overflow each scratch space (object_scratch, string_scratch, etc.) + - Verify errors are propagated correctly to the caller + + 5. **Test local scope overflow**: + - Create a test with deeply nested lexical scopes or many local variables + - Verify scope overflow errors are detected + + 6. **Test error propagation paths**: + - Test that errors propagate correctly through eval, parse, and other functions + - Verify that container errors take precedence over other errors + + 7. **Test error reporting mechanism**: + - Verify that container errors can be reported even when strings container is full + - Test fallback mechanisms for error reporting + + 8. **Integration tests**: + - Test interaction between various overflow scenarios + - Verify that the system remains in a stable state after overflow + + 9. **Test Implementation Considerations**: + - **Initialization vs. Runtime Overflow**: + - Container sizes must be large enough to accommodate built-ins + - Test both initialization failure and runtime overflow separately + + - **Testing Approaches**: + 1. **Staged Overflow Testing**: + - Start with containers just large enough for initialization + - Then incrementally add more items until each container overflows + - Use custom subclass or wrapper that exposes current capacity usage + + 2. **Container-Specific Testing**: + - For global_scope: Test with many variable definitions + - For strings: Test with many unique string literals + - For values: Test with deeply nested expressions or long lists + - For scratch spaces: Test operations that heavily use each scratch space + + 3. **Custom Construction Testing**: + - Create a test helper that allows partial initialization + - Skip adding built-ins that aren't needed for specific tests + - Use smaller containers for specific overflow scenarios + + 4. **Two-Phase Testing**: + - Phase 1: Test error detection during initialization + - Phase 2: Test error detection during evaluation + + 5. **SmallVector Insert Methods Testing**: + - Create unit tests specifically for the SmallVector class + - Test insert() with exact capacity limits to verify error_state is set correctly + - Test insert(SpanType) with values that exceed capacity + - Test insert_or_find() with values that exceed capacity + - Verify returned KeyType values are safe and valid even in error cases + - Check that partially inserted values are handled correctly + - **Expected Result**: + - Clearer error messages when capacity limits are reached + - Better debugging experience when working with constrained container sizes + - More robust error handling in embedded environments + - **Core Implementation Strategy**: + 1. **Fix SmallVector Higher-Level Insert Methods**: + ```cpp + // Current problematic implementation of insert(SpanType) + constexpr KeyType insert(SpanType values) noexcept + { + size_type last = 0; + for (const auto &value : values) { last = insert(value); } + return KeyType{ static_cast(last - values.size() + 1), static_cast(values.size()) }; + } + + // Fix: Check error_state after each insert and return a safe KeyType on error + constexpr KeyType insert(SpanType values) noexcept + { + if (values.empty()) { return KeyType{0, 0}; } // Safe empty KeyType + + const auto start_idx = small_size_used; + size_type inserted = 0; + + for (const auto &value : values) { + const auto idx = insert(value); + if (error_state) { + // We hit capacity - return a KeyType with the correct elements we did manage to insert + return KeyType{start_idx, inserted}; + } + inserted++; + } + + return KeyType{start_idx, inserted}; + } + + // Current problematic implementation of insert_or_find + constexpr KeyType insert_or_find(SpanType values) noexcept + { + if (const auto small_found = std::search(begin(), end(), values.begin(), values.end()); small_found != end()) { + return KeyType{ static_cast(std::distance(begin(), small_found)), + static_cast(values.size()) }; + } else { + return insert(values); // Doesn't check if insert succeeded + } + } + + // Fix: Check error_state after insert and handle appropriately + constexpr KeyType insert_or_find(SpanType values) noexcept + { + if (const auto small_found = std::search(begin(), end(), values.begin(), values.end()); small_found != end()) { + return KeyType{ static_cast(std::distance(begin(), small_found)), + static_cast(values.size()) }; + } else { + const auto before_error = error_state; + const auto result = insert(values); + + // If we had no error before but have one now, the insert failed + if (!before_error && error_state) { + // Could return a special error KeyType or just the best approximation we have + // For safety, might want to return KeyType{0, 0} to avoid propagating bad indices + } + + return result; + } + } + ``` + 2. **Container Error Detection**: + ```cpp + // Add method to check container error states + [[nodiscard]] constexpr bool has_container_error() const noexcept { + return global_scope.error_state || + strings.error_state || + values.error_state || + object_scratch.error_state || + variables_scratch.error_state || + string_scratch.error_state; + } + + // Add method to check scope error state + [[nodiscard]] constexpr bool has_scope_error(const LexicalScope &scope) const noexcept { + return scope.error_state; + } + + // Add method to check all error states including passed scope + [[nodiscard]] constexpr bool has_any_error(const LexicalScope &scope) const noexcept { + return has_container_error() || has_scope_error(scope); + } + ``` + + 2. **Error Checking in Evaluation**: + ```cpp + [[nodiscard]] constexpr SExpr eval(LexicalScope &scope, const SExpr expr) { + // Check for container errors first + if (has_any_error(scope)) { + return create_container_error(scope); + } + + // Existing evaluation logic... + + // Check again after evaluation + if (has_any_error(scope)) { + return create_container_error(scope); + } + + return result; + } + ``` + + - **Possible Error Reporting Approaches**: + 1. **Pre-allocation Strategy**: + - Reserve a set of predefined error strings during initialization + - Use indices instead of direct references for error messages + - This ensures error reporting never needs to allocate new strings + 2. **Error Code Strategy**: + - Define an enum of error codes (e.g., STRING_CAPACITY_EXCEEDED) + - Return error codes directly inside the Error type + - Let the hosting application map codes to messages + 3. **Two-Phase Error Reporting**: + - Add a "container_error_type" field to Error type + - When container errors occur, set numeric type without creating strings + - Only generate detailed error messages if string container has capacity + - Fall back to generic error codes when strings are full + 4. **Extend Error Type**: + - Modify Error type to hold either string reference or direct error code + - Avoid string allocation when reporting container capacity errors + - Use the direct error code path when strings container is full + - **Example Implementation Sketch**: + ```cpp + // Add error codes enum + enum struct ContainerErrorCode : std::uint8_t { + NONE, + GLOBAL_SCOPE_FULL, + STRINGS_FULL, + VALUES_FULL, + SCRATCH_SPACE_FULL + }; + + // Modify Error struct to include container error code + template struct Error { + using size_type = SizeType; + IndexedString expected; // Existing field + IndexedList got; // Existing field + ContainerErrorCode container_error{ContainerErrorCode::NONE}; // New field + + // Constructor for regular errors (unchanged) + constexpr Error(IndexedString exp, IndexedList g) + : expected(exp), got(g), container_error(ContainerErrorCode::NONE) {} + + // New constructor for container errors (no string allocation) + constexpr Error(ContainerErrorCode code) + : expected{0, 0}, got{0, 0}, container_error(code) {} + + [[nodiscard]] constexpr bool is_container_error() const { + return container_error != ContainerErrorCode::NONE; + } + }; + + // Then usage would be like: + if (strings.error_state) { + return SExpr{Error{ContainerErrorCode::STRINGS_FULL}}; + } + ``` + +## Coverage Analysis + +### How to Run Branch Coverage Report + +The project has a pre-configured `build-coverage` directory for generating coverage reports. To run a branch coverage analysis: + +```bash +# 1. Build the coverage-configured project (don't reconfigure!) +cmake --build ./build-coverage + +# 2. Run all tests to generate coverage data +cd ./build-coverage && ctest + +# 3. Generate branch coverage report for cons_expr.hpp +cd /home/jason/cons_expr/build-coverage +gcovr --txt-metric branch --filter ../include/cons_expr/cons_expr.hpp --gcov-ignore-errors=no_working_dir_found . +``` + +**Note**: The `--gcov-ignore-errors=no_working_dir_found` flag is needed to ignore errors from dependency coverage data (Catch2, etc.) that we don't need for our analysis. + +## Branch Coverage Tests to Add + +Based on coverage analysis showing 36% branch coverage for `include/cons_expr/cons_expr.hpp`, these specific test cases should be added to improve coverage to ~55-65%. + +**IMPORTANT**: All tests must use `STATIC_CHECK` and be constexpr-capable for compatibility with the `constexpr_tests` target. Follow existing test patterns in `constexpr_tests.cpp`. + +### 1. **SmallVector Overflow Tests** (Lines 187, 192) - **HIGH PRIORITY** +**File**: `constexpr_tests.cpp` +```cpp +TEST_CASE("SmallVector overflow scenarios", "[utility]") { + constexpr auto test = []() constexpr { + // Create engine with smaller capacity for testing + cons_expr<32, char, int, double> engine; // Reduced capacity + + // Test error state after exceeding capacity + for (int i = 0; i < 35; ++i) { // Exceed capacity + engine.values.insert(engine.True); + } + return engine.values.error_state; + }; + + STATIC_CHECK(test()); + + constexpr auto test2 = []() constexpr { + cons_expr<32, char, int, double> engine; + + // Test string capacity overflow + for (int i = 0; i < 100; ++i) { + std::string_view test_str = "test_string_content"; + engine.strings.insert(std::span{test_str.data(), test_str.size()}); + } + return engine.strings.error_state; + }; + + STATIC_CHECK(test2()); +} +``` + +### 2. **Number Parsing Edge Cases** (Lines 263, 283, 288, 296, 310, 319, 334, 343, 351) - **HIGH PRIORITY** +**File**: `constexpr_tests.cpp` +```cpp +TEST_CASE("Number parsing edge cases", "[parser]") { + constexpr auto test_lone_minus = []() constexpr { + // Test lone minus sign + auto result = parse_number("-"); + return !result.first; // Should fail parsing + }; + STATIC_CHECK(test_lone_minus()); + + constexpr auto test_scientific_notation = []() constexpr { + // Test 'e'/'E' notation variations + auto float_result = parse_number("123e5"); + return float_result.first && (float_result.second == 12300000.0); + }; + STATIC_CHECK(test_scientific_notation()); + + constexpr auto test_invalid_exponent = []() constexpr { + // Test invalid exponent characters + auto bad_exp = parse_number("1.5eZ"); + return !bad_exp.first; // Should fail + }; + STATIC_CHECK(test_invalid_exponent()); + + constexpr auto test_incomplete_exponent = []() constexpr { + // Test incomplete exponent (starts but no digits) + auto incomplete_exp = parse_number("1.5e"); + return !incomplete_exp.first; // Should fail + }; + STATIC_CHECK(test_incomplete_exponent()); + + constexpr auto test_negative_exponent = []() constexpr { + // Test negative exponent + auto neg_exp = parse_number("1.5e-2"); + return neg_exp.first && (neg_exp.second == 0.015); + }; + STATIC_CHECK(test_negative_exponent()); +} +``` + +### 3. **Parser Null Pointer Handling** (Lines 601, 639, 651) - **HIGH PRIORITY** +**File**: `constexpr_tests.cpp` +```cpp +TEST_CASE("Parser safety edge cases", "[parser]") { + constexpr auto test_null_pointer = []() constexpr { + cons_expr<> engine; + + // Test null sexpr in get_if + const decltype(engine)::SExpr* null_ptr = nullptr; + auto result = engine.get_if(null_ptr); + return result == nullptr; + }; + STATIC_CHECK(test_null_pointer()); + + constexpr auto test_unterminated_string = []() constexpr { + cons_expr<> engine; + + // Test unterminated string in parser + auto [parsed, remaining] = engine.parse("\"unterminated"); + if (parsed.size == 0) return false; + + auto& first_expr = engine.values[parsed[0]]; + return std::holds_alternative(first_expr.value); + }; + STATIC_CHECK(test_unterminated_string()); +} +``` + +### 4. **Token Parsing Edge Cases** (Lines 367, 372, 389, 392, 410, 415, 417) - **MEDIUM PRIORITY** +**File**: `constexpr_tests.cpp` +```cpp +TEST_CASE("Token parsing edge cases", "[parser]") { + constexpr auto test_line_endings = []() constexpr { + // Test end-of-line characters + auto token1 = next_token("\r\n token"); + return token1.parsed == "token"; + }; + STATIC_CHECK(test_line_endings()); + + constexpr auto test_quote_character = []() constexpr { + // Test quote character + auto token2 = next_token("'symbol"); + return token2.parsed == "'"; + }; + STATIC_CHECK(test_quote_character()); + + constexpr auto test_parentheses = []() constexpr { + // Test parentheses + auto token3 = next_token(")rest"); + return token3.parsed == ")"; + }; + STATIC_CHECK(test_parentheses()); + + constexpr auto test_unterminated_string_token = []() constexpr { + // Test unterminated string + auto token4 = next_token("\"unterminated string"); + return token4.parsed == "\"unterminated string"; + }; + STATIC_CHECK(test_unterminated_string_token()); + + constexpr auto test_empty_token = []() constexpr { + // Test empty token at end + auto token5 = next_token(""); + return token5.parsed.empty(); + }; + STATIC_CHECK(test_empty_token()); +} +``` + +### 5. **String Escape Processing** (Lines 494, 538, 548) - **MEDIUM PRIORITY** +**File**: `constexpr_tests.cpp` +```cpp +TEST_CASE("String escape edge cases", "[strings]") { + constexpr auto test_error_equality = []() constexpr { + cons_expr<> engine; + + // Test error type equality comparison + auto error1 = engine.make_error("test error", engine.empty_indexed_list); + auto error2 = engine.make_error("test error", engine.empty_indexed_list); + auto err1 = std::get(error1.value); + auto err2 = std::get(error2.value); + return err1 == err2; + }; + STATIC_CHECK(test_error_equality()); + + constexpr auto test_unknown_escape = []() constexpr { + cons_expr<> engine; + + // Test unknown escape character + auto bad_escape = engine.process_string_escapes("test\\q"); + return std::holds_alternative(bad_escape.value); + }; + STATIC_CHECK(test_unknown_escape()); + + constexpr auto test_unterminated_escape = []() constexpr { + cons_expr<> engine; + + // Test unterminated escape (string ends with backslash) + auto unterminated = engine.process_string_escapes("test\\"); + return std::holds_alternative(unterminated.value); + }; + STATIC_CHECK(test_unterminated_escape()); +} +``` + +### 6. **Quote Depth Handling** (Lines 745, 754, 762-773) - **MEDIUM PRIORITY** +**File**: `constexpr_tests.cpp` +```cpp +TEST_CASE("Quote depth handling", "[parser]") { + constexpr auto test_multiple_quotes = []() constexpr { + cons_expr<> engine; + + // Test multiple quote levels + auto [parsed, _] = engine.parse("'''symbol"); + return parsed.size == 1; + }; + STATIC_CHECK(test_multiple_quotes()); + + constexpr auto test_quote_booleans = []() constexpr { + cons_expr<> engine; + + // Test quote with different token types + auto [parsed2, _2] = engine.parse("'true"); + auto [parsed3, _3] = engine.parse("'false"); + return parsed2.size == 1 && parsed3.size == 1; + }; + STATIC_CHECK(test_quote_booleans()); + + constexpr auto test_quote_literals = []() constexpr { + cons_expr<> engine; + + // Test quote with strings, numbers + auto [parsed4, _4] = engine.parse("'\"hello\""); + auto [parsed5, _5] = engine.parse("'123"); + auto [parsed6, _6] = engine.parse("'123.45"); + return parsed4.size == 1 && parsed5.size == 1 && parsed6.size == 1; + }; + STATIC_CHECK(test_quote_literals()); +} +``` + +### 7. **Error Propagation** (Lines 779, 780, 784-796) - **LOWER PRIORITY** +**File**: `constexpr_tests.cpp` +```cpp +TEST_CASE("Float vs int parsing priority", "[parser]") { + constexpr auto test_float_parsing = []() constexpr { + cons_expr<> engine; + + // Test case where int parsing fails but float parsing succeeds + auto [parsed, _] = engine.parse("123.456"); + if (parsed.size == 0) return false; + + auto& expr = engine.values[parsed[0]]; + auto* atom = std::get_if(&expr.value); + if (atom == nullptr) return false; + + return std::holds_alternative(*atom); + }; + STATIC_CHECK(test_float_parsing()); + + constexpr auto test_identifier_fallback = []() constexpr { + cons_expr<> engine; + + // Test case where both int and float parsing fail + auto [parsed2, _2] = engine.parse("not_a_number"); + if (parsed2.size == 0) return false; + + auto& expr2 = engine.values[parsed2[0]]; + auto* atom2 = std::get_if(&expr2.value); + if (atom2 == nullptr) return false; + + return std::holds_alternative(*atom2); + }; + STATIC_CHECK(test_identifier_fallback()); +} +``` + +### **Implementation Priority & Expected Impact**: +1. **Phase 1**: SmallVector overflow + Number parsing + Null pointer handling (should get coverage to ~48-52%) +2. **Phase 2**: Token parsing + String escape processing (should get coverage to ~52-58%) +3. **Phase 3**: Quote depth + Error propagation (should get coverage to ~55-65%) + +### **Test Organization**: +- **ALL tests must be added to the `constexpr_tests` target** and use `STATIC_CHECK` patterns +- Tests can be added to existing test files or new test files as appropriate +- Tests must be evaluable at compile-time to work with the `constexpr_tests` target +- Follow the existing patterns in the constexpr test files for consistency +- Use reduced template parameters (e.g., `cons_expr<32, char, int, double>`) for overflow testing diff --git a/build_and_test_coverage.sh b/build_and_test_coverage.sh new file mode 100755 index 0000000..b4193e7 --- /dev/null +++ b/build_and_test_coverage.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# Debug Coverage Build and Test Script +# Usage: ./build_and_test_coverage.sh [build_folder] +# Default build folder: build-coverage-debug + +set -e # Exit on any error + +# Configuration +BUILD_DIR="${1:-build-coverage-debug}" +SOURCE_DIR="$(pwd)" + +echo "=== Debug Coverage Build and Test Script ===" +echo "Source directory: $SOURCE_DIR" +echo "Build directory: $BUILD_DIR" +echo + +# Step 1: Configure with CMake using Ninja and debug coverage (if needed) +if [ -d "$BUILD_DIR" ] && [ -f "$BUILD_DIR/build.ninja" ]; then + echo "Step 1: Build directory exists and is configured, skipping configuration..." + echo "ℹ To force reconfiguration, delete $BUILD_DIR and run again" +else + echo "Step 1: Configuring with CMake (Debug + Coverage + Ninja)..." + cmake -S . -B "$BUILD_DIR" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -Dcons_expr_ENABLE_CLANG_TIDY:Bool=ON \ + -Dcons_expr_ENABLE_IPO:Bool=ON \ + -Dcons_expr_ENABLE_HARDENING:Bool=ON \ + -Dcons_expr_ENABLE_COVERAGE:Bool=ON + echo "✓ Configuration complete" +fi +echo + +# Step 3: Verify gcov is available for coverage +echo "Step 3: Verifying coverage toolchain..." +if ! command -v gcovr >/dev/null 2>&1; then + echo "❌ ERROR: gcovr not found - coverage cannot be generated" + echo "Please install gcovr (usually part of gcc/build-essential package)" + exit 1 +fi +echo "✓ gcovr found - coverage generation enabled" +echo + +# Step 4: Build all targets except constexpr_tests in parallel +echo "Step 4: Building all targets except constexpr_tests in parallel..." +cmake --build "$BUILD_DIR" --target relaxed_constexpr_tests tests cons_expr ccons_expr speed_test + +echo "✓ All targets built successfully (except constexpr_tests)" +echo + +# Step 5: Execute relaxed_constexpr_tests +echo "Step 5: Running relaxed_constexpr_tests..." +./$BUILD_DIR/test/relaxed_constexpr_tests + +echo "✓ relaxed_constexpr_tests passed" +echo + +# Step 6: Execute tests +echo "Step 6: Running tests..." +./$BUILD_DIR/test/tests + +echo "✓ tests passed" +echo + + +# Step 2: Clean up any existing coverage files to avoid stamp mismatches +echo "Step 2: Cleaning up existing coverage files (gcda and gcno)..." +cd "$BUILD_DIR" +find . -name "*.gcda" -delete +cd - +echo "✓ Coverage files cleaned (prevents gcov stamp mismatch)" +echo + + +# Step 7: Build constexpr_tests (compile-time tests) +echo "Step 7: Building constexpr_tests (compile-time validation)..." +cmake --build $BUILD_DIR --target constexpr_tests + +echo "✓ constexpr_tests compiled successfully (all static assertions passed)" +echo + + + +# Step 8: Final build to catch any remaining tools +echo "Step 8: Final build to catch any remaining tools..." +cmake --build $BUILD_DIR +echo "✓ Final build completed" +echo + +# Step 10: Run all tests with CTest in parallel +echo "Step 10: Running all tests with CTest in parallel..." +cd $BUILD_DIR +ctest -C Debug -j +cd - + +echo "✓ All CTest tests completed" +echo + +# Step 11: Generate comprehensive coverage report with decision/call coverage and multiple output formats +echo "Step 11: Generating comprehensive coverage information..." +cd $BUILD_DIR +gcovr -k --filter ../include/cons_expr/cons_expr.hpp --exclude-directories _deps --gcov-ignore-errors=no_working_dir_found . --html --html-details --html-title "cons_expr Coverage Report" -o coverage_report.html -j 4 --decisions --calls --json=coverage_report.json --txt-summary +# Note: gcovr automatically handles .gcov file cleanup and generates multiple output formats simultaneously +cd - +echo "✓ Comprehensive coverage reports generated in build directory:" +echo " - Text summary: displayed above" +echo " - HTML detailed report: $BUILD_DIR/coverage_report.html" +echo " - JSON data report: $BUILD_DIR/coverage_report.json" + +echo +echo "=== All Steps Completed Successfully! ===" +echo "Build directory: $BUILD_DIR" +echo "Tests passed: relaxed_constexpr_tests, tests, constexpr_tests, CTest suite" +if [ -d "coverage_html" ]; then + echo "Coverage report: $BUILD_DIR/coverage_html/index.html" +fi +echo diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake new file mode 100644 index 0000000..a824732 --- /dev/null +++ b/cmake/Emscripten.cmake @@ -0,0 +1,264 @@ +# cmake/Emscripten.cmake +# Emscripten/WebAssembly build configuration + +# Common paths for web assets +set(cons_expr_WEB_DIR "${CMAKE_SOURCE_DIR}/web") +set(cons_expr_COI_WORKER "${cons_expr_WEB_DIR}/coi-serviceworker.min.js") +set(cons_expr_SHELL_TEMPLATE "${cons_expr_WEB_DIR}/shell_template.html.in") +set(cons_expr_INDEX_TEMPLATE "${cons_expr_WEB_DIR}/index_template.html.in") + +# Helper function to escape HTML special characters +function(escape_html output_var input) + set(result "${input}") + string(REPLACE "&" "&" result "${result}") + string(REPLACE "<" "<" result "${result}") + string(REPLACE ">" ">" result "${result}") + string(REPLACE "\"" """ result "${result}") + set(${output_var} "${result}" PARENT_SCOPE) +endfunction() + +# Detect if we're building with Emscripten +if(EMSCRIPTEN) + message(STATUS "Emscripten build detected - configuring for WebAssembly") + + # Set WASM build flag + set(cons_expr_WASM_BUILD ON CACHE BOOL "Building for WebAssembly" FORCE) + + # Sanitizers don't work with Emscripten + foreach(sanitizer ADDRESS LEAK UNDEFINED THREAD MEMORY) + set(cons_expr_ENABLE_SANITIZER_${sanitizer} OFF CACHE BOOL "Not supported with Emscripten") + endforeach() + + # Disable static analysis and strict warnings for Emscripten builds + foreach(option CLANG_TIDY CPPCHECK WARNINGS_AS_ERRORS) + set(cons_expr_ENABLE_${option} OFF CACHE BOOL "Disabled for Emscripten") + endforeach() + + # Disable testing - no way to execute WASM test targets + set(BUILD_TESTING OFF CACHE BOOL "No test runner for WASM") + + # WASM runtime configuration - tunable performance parameters + set(cons_expr_WASM_INITIAL_MEMORY "33554432" CACHE STRING + "Initial WASM memory in bytes (default: 32MB)") + set(cons_expr_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING + "Pthread pool size for WASM builds (default: 4)") + set(cons_expr_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING + "Asyncify stack size in bytes (default: 64KB)") + + # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling + # Set these flags early so they propagate to all dependencies + add_compile_options(-pthread -fwasm-exceptions) + add_link_options(-pthread -fwasm-exceptions) +endif() + +# Function to apply WASM settings to a target +function(cons_expr_configure_wasm_target target) + if(EMSCRIPTEN) + # Parse optional named arguments + set(options "") + set(oneValueArgs TITLE DESCRIPTION RESOURCES_DIR IO_MODE) + set(multiValueArgs "") + cmake_parse_arguments(WASM "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Set defaults if not provided + if(NOT WASM_TITLE) + set(WASM_TITLE "${target}") + endif() + + if(NOT WASM_DESCRIPTION) + set(WASM_DESCRIPTION "WebAssembly application") + endif() + + if(NOT WASM_IO_MODE) + set(WASM_IO_MODE "FTXUI") + endif() + + # Get the actual output name (may differ from target name) + get_target_property(OUTPUT_NAME ${target} OUTPUT_NAME) + if(NOT OUTPUT_NAME) + set(OUTPUT_NAME "${target}") + endif() + + # Register this target in the global WASM targets list + set_property(GLOBAL APPEND PROPERTY cons_expr_WASM_TARGETS "${target}") + set_property(GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_TITLE "${WASM_TITLE}") + set_property(GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_DESCRIPTION "${WASM_DESCRIPTION}") + set_property(GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_OUTPUT_NAME "${OUTPUT_NAME}") + + target_compile_definitions(${target} PRIVATE cons_expr_WASM_BUILD=1) + + # Emscripten link flags + target_link_options(${target} PRIVATE + # Enable pthreads - REQUIRED by FTXUI's WASM implementation + "-sUSE_PTHREADS=1" + "-sPROXY_TO_PTHREAD=1" + "-sPTHREAD_POOL_SIZE=${cons_expr_WASM_PTHREAD_POOL_SIZE}" + # Enable asyncify for emscripten_sleep and async operations + "-sASYNCIFY=1" + "-sASYNCIFY_STACK_SIZE=${cons_expr_WASM_ASYNCIFY_STACK_SIZE}" + # Memory configuration + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=${cons_expr_WASM_INITIAL_MEMORY}" + # Environment - need both web and worker for pthread support + "-sENVIRONMENT=web,worker" + # Export runtime methods for JavaScript interop + "-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']" + # Export malloc/free for MAIN_THREAD_EM_ASM usage + "-sEXPORTED_FUNCTIONS=['_main','_malloc','_free']" + # Debug: enable assertions for better error messages + "-sASSERTIONS=1" + ) + + # Embed resources into WASM binary (optional, per-target) + if(WASM_RESOURCES_DIR AND EXISTS "${WASM_RESOURCES_DIR}") + # Convert to absolute path to avoid issues with Emscripten path resolution + get_filename_component(ABS_RESOURCES_DIR "${WASM_RESOURCES_DIR}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}") + + target_link_options(${target} PRIVATE + "--embed-file=${ABS_RESOURCES_DIR}@/resources" + ) + message(STATUS "Embedding resources for ${target} from ${ABS_RESOURCES_DIR}") + endif() + + # Select appropriate shell template based on IO mode + if(WASM_IO_MODE STREQUAL "CONSOLE") + set(SHELL_TEMPLATE "${cons_expr_WEB_DIR}/shell_template_console.html.in") + message(STATUS "Using CONSOLE I/O mode for ${target} (PTY via xterm-pty)") + else() + set(SHELL_TEMPLATE "${cons_expr_WEB_DIR}/shell_template_ftxui.html.in") + message(STATUS "Using FTXUI I/O mode for ${target} (character-at-a-time)") + endif() + + # Configure the shell HTML template for this target + set(TARGET_NAME "${OUTPUT_NAME}") + set(TARGET_TITLE "${WASM_TITLE}") + set(TARGET_DESCRIPTION "${WASM_DESCRIPTION}") + set(AT "@") # For escaping @ in npm package URLs + set(CONFIGURED_SHELL "${CMAKE_BINARY_DIR}/web/${target}_shell.html") + + # Generate target-specific shell file (configure_file creates parent directories automatically) + if(EXISTS "${SHELL_TEMPLATE}") + configure_file( + "${SHELL_TEMPLATE}" + "${CONFIGURED_SHELL}" + @ONLY + ) + + # Use the generated shell file + target_link_options(${target} PRIVATE + "--shell-file=${CONFIGURED_SHELL}" + ) + + # Add both template and configured file as link dependencies + set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS + "${SHELL_TEMPLATE}" + "${CONFIGURED_SHELL}" + ) + + message(STATUS "Configured WASM shell for ${target}: ${CONFIGURED_SHELL}") + else() + message(FATAL_ERROR "Shell template not found: ${SHELL_TEMPLATE}") + endif() + + # Copy service worker to target build directory for standalone target builds + if(EXISTS "${cons_expr_COI_WORKER}") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${cons_expr_COI_WORKER}" + "$/coi-serviceworker.min.js" + COMMENT "Copying coi-serviceworker.min.js to ${target} build directory" + ) + endif() + + # Set output suffix to .html + set_target_properties(${target} PROPERTIES SUFFIX ".html") + + message(STATUS "Configured ${target} for WebAssembly") + endif() +endfunction() + +# Create a unified web deployment directory with all WASM targets +function(cons_expr_create_web_dist) + if(NOT EMSCRIPTEN) + return() + endif() + + # Define output directory + set(WEB_DIST_DIR "${CMAKE_BINARY_DIR}/web-dist") + + # Get list of all WASM targets + get_property(WASM_TARGETS GLOBAL PROPERTY cons_expr_WASM_TARGETS) + + if(NOT WASM_TARGETS) + message(WARNING "No WASM targets registered. Skipping web-dist generation.") + return() + endif() + + # Generate HTML for app cards + set(WASM_APPS_HTML "") + foreach(target ${WASM_TARGETS}) + get_property(TITLE GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_TITLE) + get_property(DESCRIPTION GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_DESCRIPTION) + + # Escape HTML special characters to prevent injection + escape_html(TITLE_ESCAPED "${TITLE}") + escape_html(DESC_ESCAPED "${DESCRIPTION}") + + string(APPEND WASM_APPS_HTML +" +

${TITLE_ESCAPED}

+

${DESC_ESCAPED}

+
+") + endforeach() + + # Generate index.html from template + set(INDEX_OUTPUT "${WEB_DIST_DIR}/index.html") + + if(EXISTS "${cons_expr_INDEX_TEMPLATE}") + configure_file("${cons_expr_INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) + else() + message(WARNING "Index template not found: ${cons_expr_INDEX_TEMPLATE}") + endif() + + # Build list of copy commands + set(COPY_COMMANDS "") + + # For each WASM target, copy artifacts to subdirectory + # Each target gets its own service worker copy for standalone deployment + foreach(target ${WASM_TARGETS}) + get_target_property(TARGET_BINARY_DIR ${target} BINARY_DIR) + get_property(OUTPUT_NAME GLOBAL PROPERTY cons_expr_WASM_TARGET_${target}_OUTPUT_NAME) + set(TARGET_DIST_DIR "${WEB_DIST_DIR}/${target}") + + # Copy WASM artifacts: .html (as index.html), .js, .wasm, and service worker + # Use OUTPUT_NAME instead of target name for file names + list(APPEND COPY_COMMANDS + COMMAND ${CMAKE_COMMAND} -E make_directory "${TARGET_DIST_DIR}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${TARGET_BINARY_DIR}/${OUTPUT_NAME}.html" + "${TARGET_DIST_DIR}/index.html" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${TARGET_BINARY_DIR}/${OUTPUT_NAME}.js" + "${TARGET_DIST_DIR}/${OUTPUT_NAME}.js" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${TARGET_BINARY_DIR}/${OUTPUT_NAME}.wasm" + "${TARGET_DIST_DIR}/${OUTPUT_NAME}.wasm" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${cons_expr_COI_WORKER}" + "${TARGET_DIST_DIR}/coi-serviceworker.min.js" + ) + endforeach() + + # Create custom target with all commands (part of ALL so it builds by default) + add_custom_target(web-dist ALL + COMMAND ${CMAKE_COMMAND} -E make_directory "${WEB_DIST_DIR}" + ${COPY_COMMANDS} + COMMENT "Creating unified web deployment directory" + ) + + # Ensure web-dist runs after all WASM targets are built + add_dependencies(web-dist ${WASM_TARGETS}) + + message(STATUS "Configured web-dist target with ${WASM_TARGETS}") +endfunction() diff --git a/cmake/Hardening.cmake b/cmake/Hardening.cmake index 91f2f20..d3a2b8d 100644 --- a/cmake/Hardening.cmake +++ b/cmake/Hardening.cmake @@ -9,16 +9,16 @@ macro( message(STATUS "** Enabling Hardening (Target ${target}) **") if(MSVC) - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} /sdl /DYNAMICBASE /guard:cf") + list(APPEND NEW_COMPILE_OPTIONS /sdl /DYNAMICBASE /guard:cf) message(STATUS "*** MSVC flags: /sdl /DYNAMICBASE /guard:cf /NXCOMPAT /CETCOMPAT") - set(NEW_LINK_OPTIONS "${NEW_LINK_OPTIONS} /NXCOMPAT /CETCOMPAT") + list(APPEND NEW_LINK_OPTIONS /NXCOMPAT /CETCOMPAT) elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang|GNU") - set(NEW_CXX_DEFINITIONS "${NEW_CXX_DEFINITIONS} -D_GLIBCXX_ASSERTIONS") + list(APPEND NEW_CXX_DEFINITIONS -D_GLIBCXX_ASSERTIONS) message(STATUS "*** GLIBC++ Assertions (vector[], string[], ...) enabled") - if (NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug") - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3") + if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + list(APPEND NEW_COMPILE_OPTIONS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3) message(STATUS "*** g++/clang _FORTIFY_SOURCE=3 enabled") endif() @@ -34,7 +34,7 @@ macro( check_cxx_compiler_flag(-fstack-protector-strong STACK_PROTECTOR) if(STACK_PROTECTOR) - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} -fstack-protector-strong") + list(APPEND NEW_COMPILE_OPTIONS -fstack-protector-strong) message(STATUS "*** g++/clang -fstack-protector-strong enabled") else() message(STATUS "*** g++/clang -fstack-protector-strong NOT enabled (not supported)") @@ -42,7 +42,7 @@ macro( check_cxx_compiler_flag(-fcf-protection CF_PROTECTION) if(CF_PROTECTION) - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} -fcf-protection") + list(APPEND NEW_COMPILE_OPTIONS -fcf-protection) message(STATUS "*** g++/clang -fcf-protection enabled") else() message(STATUS "*** g++/clang -fcf-protection NOT enabled (not supported)") @@ -51,7 +51,7 @@ macro( check_cxx_compiler_flag(-fstack-clash-protection CLASH_PROTECTION) if(CLASH_PROTECTION) if(LINUX OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} -fstack-clash-protection") + list(APPEND NEW_COMPILE_OPTIONS -fstack-clash-protection) message(STATUS "*** g++/clang -fstack-clash-protection enabled") else() message(STATUS "*** g++/clang -fstack-clash-protection NOT enabled (clang on non-Linux)") @@ -65,12 +65,12 @@ macro( check_cxx_compiler_flag("-fsanitize=undefined -fno-sanitize-recover=undefined -fsanitize-minimal-runtime" MINIMAL_RUNTIME) if(MINIMAL_RUNTIME) - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} -fsanitize=undefined -fsanitize-minimal-runtime") - set(NEW_LINK_OPTIONS "${NEW_LINK_OPTIONS} -fsanitize=undefined -fsanitize-minimal-runtime") + list(APPEND NEW_COMPILE_OPTIONS -fsanitize=undefined -fsanitize-minimal-runtime) + list(APPEND NEW_LINK_OPTIONS -fsanitize=undefined -fsanitize-minimal-runtime) if(NOT ${global}) - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} -fno-sanitize-recover=undefined") - set(NEW_LINK_OPTIONS "${NEW_LINK_OPTIONS} -fno-sanitize-recover=undefined") + list(APPEND NEW_COMPILE_OPTIONS -fno-sanitize-recover=undefined) + list(APPEND NEW_LINK_OPTIONS -fno-sanitize-recover=undefined) else() message(STATUS "** not enabling -fno-sanitize-recover=undefined for global consumption") endif() @@ -89,9 +89,9 @@ macro( if(${global}) message(STATUS "** Setting hardening options globally for all dependencies") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${NEW_COMPILE_OPTIONS}") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${NEW_LINK_OPTIONS}") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${NEW_CXX_DEFINITIONS}") + add_compile_options(${NEW_COMPILE_OPTIONS}) + add_compile_definitions(${NEW_CXX_DEFINITIONS}) + add_link_options(${NEW_LINK_OPTIONS}) else() target_compile_options(${target} INTERFACE ${NEW_COMPILE_OPTIONS}) target_link_options(${target} INTERFACE ${NEW_LINK_OPTIONS}) diff --git a/cmake/StaticAnalyzers.cmake b/cmake/StaticAnalyzers.cmake index a853482..324aeef 100644 --- a/cmake/StaticAnalyzers.cmake +++ b/cmake/StaticAnalyzers.cmake @@ -26,6 +26,7 @@ macro(cons_expr_enable_cppcheck WARNINGS_AS_ERRORS CPPCHECK_OPTIONS) # ignores code that cppcheck thinks is invalid C++ --suppress=syntaxError --suppress=preprocessorErrorDirective + --suppress=normalCheckLevelMaxBranches --inconclusive) else() # if the user provides a CPPCHECK_OPTIONS with a template specified, it will override this template @@ -74,6 +75,11 @@ macro(cons_expr_enable_clang_tidy target WARNINGS_AS_ERRORS) -extra-arg=-Wno-unknown-warning-option -extra-arg=-Wno-ignored-optimization-argument -extra-arg=-Wno-unused-command-line-argument + -extra-arg=-Wno-unknown-argument + -extra-arg=-Wno-gcc-compat + -extra-arg=-Wno-gcc-compat + -extra-arg=-fconstexpr-steps=12712420 + --quiet -p) # set standard if(NOT diff --git a/cmake/Tests.cmake b/cmake/Tests.cmake index 89d98a0..24d7dbd 100644 --- a/cmake/Tests.cmake +++ b/cmake/Tests.cmake @@ -2,5 +2,43 @@ function(cons_expr_enable_coverage project_name) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") target_compile_options(${project_name} INTERFACE --coverage -O0 -g) target_link_libraries(${project_name} INTERFACE --coverage) + + # Create a custom target for generating coverage reports + if(cons_expr_ENABLE_COVERAGE) + add_custom_target( + coverage_report + # First reset coverage data + COMMAND find . -name "*.gcda" -delete + COMMAND find . -name "coverage.info" -delete + + # Run the tests + COMMAND ctest -C Debug + # Use a separate script to run the coverage commands + COMMAND lcov --capture --directory . --output-file coverage.info --exclude \"${CMAKE_SOURCE_DIR}/test/*\" --exclude \"/usr/*\" --exclude \"${CMAKE_BINARY_DIR}/_deps/*\" --output-file coverage.info + COMMAND genhtml coverage.info --output-directory coverage_report + COMMAND lcov --list coverage.info | tee coverage_summary.txt + COMMENT "Resetting coverage counters, running tests, and generating coverage report" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + add_custom_command( + TARGET coverage_report + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Coverage report generated in ${CMAKE_BINARY_DIR}/coverage_report/index.html" + ) + + # Add a test that will fail if cons_expr.hpp doesn't have 100% coverage + # add_test( + # NAME verify_cons_expr_coverage + # COMMAND cat coverage_summary.txt + # WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + #) + + # Set the test to fail if cons_expr.hpp has less than 100% coverage + # The pattern looks for "cons_expr.hpp" followed by any percentage that is not 100.0% + #set_tests_properties(verify_cons_expr_coverage PROPERTIES + # DEPENDS coverage_report + # FAIL_REGULAR_EXPRESSION "cons_expr\\.hpp[^|]*[^1]?[^0]?[^0]\\.[^0]%" + #) + endif() endif() endfunction() diff --git a/examples/compile_test.cpp b/examples/compile_test.cpp index 2ce3575..e0d5d46 100644 --- a/examples/compile_test.cpp +++ b/examples/compile_test.cpp @@ -1,8 +1,13 @@ #include +#include +#include #include +#include +#include using cons_expr_type = lefticus::cons_expr; +namespace { constexpr long long add(long long x, long long y) { return x + y; } consteval auto make_scripted_function() @@ -24,11 +29,11 @@ consteval auto make_scripted_function() )"; - [[maybe_unused]] const auto result = evaluator.sequence( - evaluator.global_scope, std::get(evaluator.parse(input).first.value)); + [[maybe_unused]] const auto result = evaluator.sequence(evaluator.global_scope, evaluator.parse(input).first); return std::bind_front(evaluator.make_callable("sum"), evaluator); } +}// namespace int main() @@ -41,7 +46,7 @@ int main() std::puts(std::format("sum({} to {}) = {}", from, to, func(from, to).value()).c_str()); }; - print_sum(101, 132414); - print_sum(1, 1222222); - print_sum(-10, 10); + print_sum(101, 132414);// NOLINT these values are arbitrary + print_sum(1, 1222222);// NOLINT + print_sum(-10, 10);// NOLINT } diff --git a/examples/speed_test.cpp b/examples/speed_test.cpp index 5caa062..5100ffa 100644 --- a/examples/speed_test.cpp +++ b/examples/speed_test.cpp @@ -1,9 +1,13 @@ #include +#include #include +#include + +namespace { constexpr long long add(long long x, long long y) { return x + y; } -void display(long long i) { std::cout << i << '\n'; } +void display(long long value) { std::cout << value << '\n'; } using cons_expr_type = lefticus::cons_expr; @@ -14,14 +18,14 @@ auto evaluate(std::string_view input) evaluator.add<&add>("add"); evaluator.add<&display>("display"); - return evaluator.sequence( - evaluator.global_scope, std::get(evaluator.parse(input).first.value)); + return evaluator.sequence(evaluator.global_scope, evaluator.parse(input).first); } template Result evaluate_to(std::string_view input) { - return std::get(std::get::Atom>(evaluate(input).value)); + return std::get(std::get(evaluate(input).value)); } +}// namespace int main() { diff --git a/fuzz_test/CMakeLists.txt b/fuzz_test/CMakeLists.txt index 60e096b..6ce8028 100644 --- a/fuzz_test/CMakeLists.txt +++ b/fuzz_test/CMakeLists.txt @@ -8,7 +8,7 @@ target_link_libraries( fuzz_tester PRIVATE cons_expr_options cons_expr_warnings - fmt::fmt + cons_expr -coverage -fsanitize=fuzzer) target_compile_options(fuzz_tester PRIVATE -fsanitize=fuzzer) diff --git a/fuzz_test/fuzz_tester.cpp b/fuzz_test/fuzz_tester.cpp index ed0fc4c..0d99936 100644 --- a/fuzz_test/fuzz_tester.cpp +++ b/fuzz_test/fuzz_tester.cpp @@ -1,22 +1,23 @@ -#include +#include +#include +#include #include -#include +#include -[[nodiscard]] auto sum_values(const uint8_t *Data, size_t Size) +// Fuzzer that tests the cons_expr parser and evaluator +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { - constexpr auto scale = 1000; + const std::string script(data, std::next(data, static_cast(size))); - int value = 0; - for (std::size_t offset = 0; offset < Size; ++offset) { - value += static_cast(*std::next(Data, static_cast(offset))) * scale; - } - return value; -} + // Initialize the cons_expr evaluator + lefticus::cons_expr<> evaluator; -// Fuzzer that attempts to invoke undefined behavior for signed integer overflow -// cppcheck-suppress unusedFunction symbolName=LLVMFuzzerTestOneInput -extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) -{ - fmt::print("Value sum: {}, len{}\n", sum_values(Data, Size), Size); - return 0; + // Try to parse the script + auto [parse_result, remaining] = evaluator.parse(script); + + // Evaluate the parsed expression + // Don't care about the result, just want to make sure nothing crashes + [[maybe_unused]] auto result = evaluator.sequence(evaluator.global_scope, parse_result); + + return 0;// Non-zero return values are reserved for future use } diff --git a/include/cons_expr/cons_expr.hpp b/include/cons_expr/cons_expr.hpp index b7c0fa2..3f5e848 100644 --- a/include/cons_expr/cons_expr.hpp +++ b/include/cons_expr/cons_expr.hpp @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2023-2024 Jason Turner +Copyright (c) 2023-2025 Jason Turner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -85,13 +85,25 @@ SOFTWARE. // * no exceptions or dynamic allocations /// Notes -// it's a scheme-like language with a few caveats: +// This is a scheme-like language with a few caveats: // * Once an object is captured or used, it's immutable // * `==` `true` and `false` stray from `=` `#t` and `#f` of scheme // * Pair types don't exist, only lists -// * only indices and values are passed, for safety during resize of `values` object +// * Only indices and values are passed, for safety during resize of `values` object // Triviality of types is critical to design and structure of this system // Triviality lets us greatly simplify the copy/move/forward discussion +// +// Supported Scheme Features: +// * Core Data Types: numbers (int/float), strings, booleans, lists, symbols +// * List Operations: car, cdr, cons, append, list, quote +// * Control Structures: if, cond, begin +// * Variable Binding: let, define +// * Functions: lambda, apply +// * Higher-order Functions: for-each +// * Evaluation Control: eval +// * Basic Arithmetic: +, -, *, / +// * Comparisons: <, >, ==, !=, <=, >= +// * Boolean Logic: and, or, not /// To do // * We probably want some sort of "defragment" at some point @@ -153,17 +165,11 @@ struct SmallVector [[nodiscard]] constexpr Contained &operator[](size_type index) noexcept { return small[index]; } [[nodiscard]] constexpr const Contained &operator[](size_type index) const noexcept { return small[index]; } [[nodiscard]] constexpr auto size() const noexcept { return small_size_used; } - [[nodiscard]] constexpr auto begin() const noexcept { return small.begin(); } - [[nodiscard]] constexpr auto begin() noexcept { return small.begin(); } - - [[nodiscard]] constexpr auto end() const noexcept - { - return std::next(small.begin(), static_cast(small_size_used)); - } + [[nodiscard]] constexpr auto begin(this auto &Self) noexcept { return Self.small.begin(); } - [[nodiscard]] constexpr auto end() noexcept + [[nodiscard]] constexpr auto end(this auto &Self) noexcept { - return std::next(small.begin(), static_cast(small_size_used)); + return std::next(Self.small.begin(), static_cast(Self.small_size_used)); } [[nodiscard]] constexpr SpanType view(KeyType range) const noexcept @@ -192,7 +198,6 @@ struct SmallVector } } - constexpr KeyType insert_or_find(SpanType values) noexcept { if (const auto small_found = std::search(begin(), end(), values.begin(), values.end()); small_found != end()) { @@ -251,6 +256,7 @@ template Token(std::basic_string_view, std::basic_string_view) -> Token; template + requires std::is_signed_v [[nodiscard]] constexpr std::pair parse_number(std::basic_string_view input) noexcept { static constexpr std::pair failure{ false, 0 }; @@ -268,51 +274,48 @@ template T value_sign = 1; long long value = 0LL; long long frac = 0LL; - long long frac_exp = 0LL; + long long frac_digits = 0LL; long long exp_sign = 1LL; long long exp = 0LL; - constexpr auto pow_10 = [](long long power) noexcept { - auto result = T{ 1 }; - if (power > 0) { - for (int iteration = 0; iteration < power; ++iteration) { result *= T{ 10 }; } - } else if (power < 0) { - for (int iteration = 0; iteration > power; --iteration) { result /= T{ 10 }; } - } + constexpr auto pow_10 = [](std::integral auto power) noexcept { + auto result = 1ll; + for (int iteration = 0; iteration < power; ++iteration) { result *= 10ll; } return result; }; const auto parse_digit = [](auto &cur_value, auto ch) { if (ch >= chars::ch('0') && ch <= chars::ch('9')) { - cur_value = cur_value * 10 + ch - chars::ch('0'); + cur_value = (cur_value * 10) + ch - chars::ch('0'); return true; - } else { - return false; } + return false; }; for (const auto ch : input) { switch (state) { case State::Start: + state = State::IntegerPart; if (ch == chars::ch('-')) { value_sign = -1; + } else if (ch == chars::ch('.')) { + state = State::FractionPart; } else if (!parse_digit(value, ch)) { return failure; } - state = State::IntegerPart; break; case State::IntegerPart: if (ch == chars::ch('.')) { state = State::FractionPart; } else if (ch == chars::ch('e') || ch == chars::ch('E')) { - state = State::ExponentPart; + state = State::ExponentStart; } else if (!parse_digit(value, ch)) { return failure; } break; case State::FractionPart: if (parse_digit(frac, ch)) { - frac_exp--; + ++frac_digits; } else if (ch == chars::ch('e') || ch == chars::ch('E')) { state = State::ExponentStart; } else { @@ -334,13 +337,25 @@ template if constexpr (std::is_integral_v) { if (state != State::IntegerPart) { return failure; } + return { true, value_sign * static_cast(value) }; } else { - if (state == State::Start || state == State::ExponentStart) { return { false, 0 }; } + if (state == State::Start || state == State::ExponentStart) { return failure; } + + const auto integral_part = static_cast(value); + const auto floating_point_part = static_cast(frac) / static_cast(pow_10(frac_digits)); + const auto signed_shifted_number = (integral_part + floating_point_part) * value_sign; + const auto shift = exp_sign * exp; + + const auto number = [&]() { + if (shift < 0) { + return signed_shifted_number / static_cast(pow_10(-shift)); + } else { + return signed_shifted_number * static_cast(pow_10(shift)); + } + }(); - return { true, - (static_cast(value_sign) * (static_cast(value) + static_cast(frac) * pow_10(frac_exp)) - * pow_10(exp_sign * exp)) }; + return { true, number }; } } @@ -370,12 +385,12 @@ template [[nodiscard]] constexpr Token next_token(s input = consume(input, is_whitespace); } + // quote + if (input.starts_with(chars::ch('\''))) { return make_token(input, 1); } + // list if (input.starts_with(chars::ch('(')) || input.starts_with(chars::ch(')'))) { return make_token(input, 1); } - // literal list - if (input.starts_with(chars::str("'("))) { return make_token(input, 2); } - // quoted string if (input.starts_with(chars::ch('"'))) { bool in_escape = false; @@ -402,19 +417,41 @@ template [[nodiscard]] constexpr Token next_token(s return make_token(input, static_cast(std::distance(input.begin(), value.begin()))); } -template struct IndexedString +// Tagged string base template +template struct TaggedIndexedString { using size_type = SizeType; size_type start{ 0 }; size_type size{ 0 }; - [[nodiscard]] constexpr bool operator==(const IndexedString &) const noexcept = default; + [[nodiscard]] constexpr bool operator==(const TaggedIndexedString &) const noexcept = default; [[nodiscard]] constexpr auto front() const noexcept { return start; } [[nodiscard]] constexpr auto substr(const size_type from) const noexcept { - return IndexedString{ static_cast(start + from), static_cast(size - from) }; + return TaggedIndexedString{ static_cast(start + from), static_cast(size - from) }; } }; +// Type aliases for the concrete string types +template using IndexedString = TaggedIndexedString; +template using Identifier = TaggedIndexedString; +template using Symbol = TaggedIndexedString; + +template +[[nodiscard]] constexpr auto to_string(const TaggedIndexedString input) +{ + return IndexedString{ input.start, input.size }; +} +template +[[nodiscard]] constexpr auto to_identifier(const TaggedIndexedString input) +{ + return Identifier{ input.start, input.size }; +} +template +[[nodiscard]] constexpr auto to_symbol(const TaggedIndexedString input) +{ + return Symbol{ input.start, input.size }; +} + template struct IndexedList { using size_type = SizeType; @@ -448,27 +485,6 @@ template struct LiteralList [[nodiscard]] constexpr bool operator==(const LiteralList &) const noexcept = default; }; -template LiteralList(IndexedList) -> LiteralList; - - -template struct Identifier -{ - using size_type = SizeType; - IndexedString value; - [[nodiscard]] constexpr auto substr(const size_type from) const { return Identifier{ value.substr(from) }; } - [[nodiscard]] constexpr bool operator==(const Identifier &other) const noexcept = default; -}; - -template struct Symbol -{ - using size_type = SizeType; - IndexedString value; - [[nodiscard]] constexpr auto substr(const size_type from) const { return Identifier{ value.substr(from) }; } - [[nodiscard]] constexpr bool operator==(const Symbol &other) const noexcept = default; -}; - -template Identifier(IndexedString) -> Identifier; - template struct Error { @@ -494,7 +510,7 @@ struct cons_expr using char_type = CharType; using size_type = SizeType; using int_type = IntegralType; - using float_type = FloatType; + using real_type = FloatType;// Using 'real' as per mathematical/Scheme convention for floating-point using string_type = IndexedString; using string_view_type = std::basic_string_view; using identifier_type = Identifier; @@ -503,6 +519,7 @@ struct cons_expr using literal_list_type = LiteralList; using error_type = Error; + template using stack_vector = SmallVector>; struct SExpr; @@ -528,14 +545,14 @@ struct cons_expr { SExpr result{}; // || will make this short circuit and stop on first matching function - ((visit_helper(result, visitor, value) || ...)); + [[maybe_unused]] const auto matched = ((visit_helper(result, visitor, value) || ...)); return result; } using LexicalScope = SmallVector, BuiltInSymbolsSize, list_type>; using function_ptr = SExpr (*)(cons_expr &, LexicalScope &, list_type); using Atom = - std::variant; + std::variant; struct FunctionPtr { @@ -568,12 +585,21 @@ struct cons_expr [[nodiscard]] constexpr bool operator==(const SExpr &) const noexcept = default; }; + + static constexpr IndexedList empty_indexed_list{ 0, 0 }; + static constexpr SExpr True{ Atom{ true } }; + static constexpr SExpr False{ Atom{ false } }; + + static_assert(std::is_trivially_copyable_v && std::is_trivially_destructible_v, "cons_expr does not work with non-trivial types"); template [[nodiscard]] static constexpr const Result *get_if(const SExpr *sexpr) noexcept { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnull-dereference" if (sexpr == nullptr) { return nullptr; } +#pragma GCC diagnostic pop if constexpr (is_sexpr_type_v) { return std::get_if(&sexpr->value); @@ -638,24 +664,31 @@ struct cons_expr { list_type parameter_names; list_type statements; + identifier_type self_identifier{ 0, 0 };// Optional identifier for recursion, default to empty [[nodiscard]] constexpr bool operator==(const Closure &) const = default; + // Check if this closure has a valid self-reference + [[nodiscard]] constexpr bool has_self_reference() const { return self_identifier.size > 0; } + [[nodiscard]] constexpr SExpr invoke(cons_expr &engine, LexicalScope &scope, list_type params) const { if (params.size != parameter_names.size) { return engine.make_error(str("Incorrect number of params for lambda"), params); } - // Closures contain all of their own scope - LexicalScope param_scope = scope; + // Create a clean scope that only contains what's needed + LexicalScope param_scope{}; - // overwrite scope with the things we know we need params to be named + // Add the self-reference first if needed (for recursion) + if (has_self_reference()) { + // Create a temporary SExpr with this closure to enable recursion + param_scope.emplace_back(to_string(self_identifier), SExpr{ *this }); + } - // set up params - // technically I'm evaluating the params lazily while invoking the lambda, not before. Does it matter? + // Set up params for (const auto [name, parameter] : std::views::zip(engine.values[parameter_names], engine.values[params])) { - param_scope.emplace_back(engine.get_if(&name)->value, engine.eval(scope, parameter)); + param_scope.emplace_back(to_string(*engine.get_if(&name)), engine.eval(scope, parameter)); } // TODO set up tail call elimination for last element of the sequence being evaluated? @@ -663,54 +696,104 @@ struct cons_expr } }; - [[nodiscard]] constexpr std::pair> parse(string_view_type input) + // Process escape sequences in a string literal + [[nodiscard]] constexpr SExpr process_string_escapes(string_view_type input) + { + // Create a temporary buffer for the processed string + // Using 64 as a reasonable initial size for most string literals + SmallVector temp_buffer{}; + + bool in_escape = false; + for (const auto &ch : input) { + if (in_escape) { + // clang-format off + switch (ch) { + case '"': temp_buffer.push_back('"'); break;// Escaped quote + case '\\': temp_buffer.push_back('\\'); break;// Escaped backslash + case 'n': temp_buffer.push_back('\n'); break;// Newline + case 't': temp_buffer.push_back('\t'); break;// Tab + case 'r': temp_buffer.push_back('\r'); break;// Carriage return + case 'f': temp_buffer.push_back('\f'); break;// Form feed + case 'b': temp_buffer.push_back('\b'); break;// Backspace + default: + return make_error(str("unexpected escape character"), strings.insert_or_find(input)); + } + // clang-format on + in_escape = false; + } else if (ch == '\\') { + in_escape = true; + } else { + temp_buffer.push_back(ch); + } + } + + // Check if we ended in an escape state (string ends with a backslash) + if (in_escape) { return make_error(str("unterminated escape sequence"), strings.insert_or_find(input)); } + + // Now use insert_or_find to deduplicate the processed string + const string_view_type processed_view(temp_buffer.small.data(), temp_buffer.size()); + return SExpr{ Atom(strings.insert_or_find(processed_view)) }; + } + + [[nodiscard]] constexpr SExpr make_quote(int quote_depth, SExpr input) + { + if (quote_depth == 0) { return input; } + + SExpr first = SExpr{ Atom{ to_identifier(strings.insert_or_find(str("quote"))) } }; + SExpr second = make_quote(quote_depth - 1, input); + std::array new_quote = { first, second }; + return SExpr{ values.insert_or_find(new_quote) }; + } + + [[nodiscard]] constexpr std::pair> parse(string_view_type input) { Scratch retval{ object_scratch }; auto token = next_token(input); + int quote_depth = 0; + while (!token.parsed.empty()) { + bool entered_quote = false; + if (token.parsed == str("(")) { auto [parsed, remaining] = parse(token.remaining); - retval.push_back(parsed); - token = remaining; - } else if (token.parsed == str("'(")) { - auto [parsed, remaining] = parse(token.remaining); - if (const auto *list = std::get_if(&parsed.value); list != nullptr) { - retval.push_back(SExpr{ LiteralList{ *list } }); - } else { - retval.push_back(make_error(str("parsed list"), parsed)); - } + retval.push_back(make_quote(quote_depth, SExpr{ parsed })); token = remaining; + } else if (token.parsed == str("'")) { + ++quote_depth; + entered_quote = true; } else if (token.parsed == str(")")) { break; } else if (token.parsed == str("true")) { - retval.push_back(SExpr{ Atom{ true } }); + retval.push_back(make_quote(quote_depth, True)); } else if (token.parsed == str("false")) { - retval.push_back(SExpr{ Atom{ false } }); + retval.push_back(make_quote(quote_depth, False)); } else { if (token.parsed.starts_with('"')) { - // note that this doesn't remove escaped characters like it should yet - // quoted string + // Process quoted string with proper escape character handling if (token.parsed.ends_with('"')) { - const auto string = strings.insert_or_find(token.parsed.substr(1, token.parsed.size() - 2)); - retval.push_back(SExpr{ Atom(string) }); + // Extract the string content (remove surrounding quotes) + const string_view_type raw_content = token.parsed.substr(1, token.parsed.size() - 2); + retval.push_back(make_quote(quote_depth, process_string_escapes(raw_content))); } else { retval.push_back(make_error(str("terminated string"), SExpr{ Atom(strings.insert_or_find(token.parsed)) })); } } else if (auto [int_did_parse, int_value] = parse_number(token.parsed); int_did_parse) { - retval.push_back(SExpr{ Atom(int_value) }); - } else if (auto [float_did_parse, float_value] = parse_number(token.parsed); float_did_parse) { - retval.push_back(SExpr{ Atom(float_value) }); - } else if (token.parsed.starts_with('\'')) { - retval.push_back(SExpr{ Atom(Symbol{ strings.insert_or_find(token.parsed.substr(1)) }) }); + retval.push_back(make_quote(quote_depth, SExpr{ Atom(int_value) })); + } else if (auto [float_did_parse, float_value] = parse_number(token.parsed); float_did_parse) { + retval.push_back(make_quote(quote_depth, SExpr{ Atom(float_value) })); } else { - retval.push_back(SExpr{ Atom(Identifier{ strings.insert_or_find(token.parsed) }) }); + const auto identifier = SExpr{ Atom(to_identifier(strings.insert_or_find(token.parsed))) }; + retval.push_back(make_quote(quote_depth, identifier)); } } + + if (!entered_quote) { quote_depth = 0; } + token = next_token(token.remaining); } - return std::pair>(SExpr{ values.insert_or_find(retval) }, token); + return { values.insert_or_find(retval), token }; } // Guaranteed to be initialized at compile time @@ -733,7 +816,6 @@ struct cons_expr add(str("for-each"), SExpr{ FunctionPtr{ for_each, FunctionPtr::Type::other } }); add(str("list"), SExpr{ FunctionPtr{ list, FunctionPtr::Type::other } }); add(str("lambda"), SExpr{ FunctionPtr{ lambda, FunctionPtr::Type::lambda_expr } }); - add(str("do"), SExpr{ FunctionPtr{ doer, FunctionPtr::Type::do_expr } }); add(str("define"), SExpr{ FunctionPtr{ definer, FunctionPtr::Type::define_expr } }); add(str("let"), SExpr{ FunctionPtr{ letter, FunctionPtr::Type::let_expr } }); add(str("car"), SExpr{ FunctionPtr{ car, FunctionPtr::Type::other } }); @@ -743,6 +825,27 @@ struct cons_expr add(str("eval"), SExpr{ FunctionPtr{ evaler, FunctionPtr::Type::other } }); add(str("apply"), SExpr{ FunctionPtr{ applier, FunctionPtr::Type::other } }); add(str("quote"), SExpr{ FunctionPtr{ quoter, FunctionPtr::Type::other } }); + add(str("begin"), SExpr{ FunctionPtr{ begin, FunctionPtr::Type::other } }); + add(str("cond"), SExpr{ FunctionPtr{ cond, FunctionPtr::Type::other } }); + add(str("error?"), SExpr{ FunctionPtr{ error_p, FunctionPtr::Type::other } }); + + // Type predicates using the generic make_type_predicate function + // Simple atomic types + add(str("integer?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + add(str("real?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + add(str("string?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + add(str("symbol?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + add(str("boolean?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + + // Composite type predicates + add(str("number?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + add(str("list?"), + SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + add( + str("procedure?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); + + // Even atom? can use the generic predicate with Atom + add(str("atom?"), SExpr{ FunctionPtr{ make_type_predicate(), FunctionPtr::Type::other } }); } [[nodiscard]] constexpr SExpr sequence(LexicalScope &scope, list_type expressions) @@ -848,7 +951,7 @@ struct cons_expr } } else if (const auto *id = get_if(&expr); id != nullptr) { for (const auto &[key, value] : scope | std::views::reverse) { - if (key == id->value) { return value; } + if (key == to_string(*id)) { return value; } } return make_error(str("id not found"), expr); @@ -876,12 +979,26 @@ struct cons_expr } } } - if (const auto *err = std::get_if(&expr.value); err != nullptr) { return std::unexpected(expr); } - return eval_to(scope, eval(scope, expr)); + + if (std::holds_alternative(expr.value) || std::holds_alternative(expr.value)) { + // no where to go from here + return std::unexpected(expr); + } + + // if things aren't changing, then we abort, because it's not going to happen + // this should be cleaned up somehow to avoid move + if (auto next = eval(scope, expr); next == expr) { + return std::unexpected(expr); + } else { + return eval_to(scope, std::move(next)); + } } + // (list 1 2 3) -> '(1 2 3) + // (list (+ 1 2) (+ 3 4)) -> '(3 7) [[nodiscard]] static constexpr SExpr list(cons_expr &engine, LexicalScope &scope, list_type params) { + // Evaluate each parameter and add it to a new list Scratch result{ engine.object_scratch }; for (const auto ¶m : engine.values[params]) { result.push_back(engine.eval(scope, param)); } return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; @@ -892,29 +1009,38 @@ struct cons_expr Scratch retval{ string_scratch }; if (auto *parameter_list = get_if(&sexpr); parameter_list != nullptr) { for (const auto &expr : values[*parameter_list]) { - if (auto *local_id = get_if(&expr); local_id != nullptr) { retval.push_back(local_id->value); } + if (auto *local_id = get_if(&expr); local_id != nullptr) { + retval.push_back(to_string(*local_id)); + } } } return retval; } + // (lambda (x y) (+ x y)) -> # + // ((lambda (x) (* x x)) 5) -> 25 [[nodiscard]] static constexpr SExpr lambda(cons_expr &engine, LexicalScope &scope, list_type params) { if (params.size < 2) { return engine.make_error(str("(lambda ([params...]) [statement...])"), params); } + // Extract parameter names from first argument auto locals = engine.get_lambda_parameter_names(engine.values[params[0]]); - // replace all references to captured values with constant copies - // this is how we create the closure object + // Replace all references to captured values with constant copies + // This is how we create the closure object - by fixing all identifiers Scratch fixed_statements{ engine.object_scratch }; for (const auto &statement : engine.values[params.sublist(1)]) { - // all of current scope is const and capturable + // All of current scope is const and capturable fixed_statements.push_back(engine.fix_identifiers(statement, locals, scope)); } + // Create the closure with parameter list and fixed statements const auto list = engine.get_if(&engine.values[params[0]]); - if (list) { return SExpr{ Closure{ *list, { engine.values.insert_or_find(fixed_statements) } } }; } + if (list) { + // Create a basic closure without self-reference initially + return SExpr{ Closure{ *list, { engine.values.insert_or_find(fixed_statements) } } }; + } return engine.make_error(str("(lambda ([params...]) [statement...])"), params); } @@ -942,56 +1068,6 @@ struct cons_expr return values[*list]; } - [[nodiscard]] constexpr SExpr fix_do_identifiers(list_type list, - size_type first_index, - std::span local_identifiers, - const LexicalScope &local_constants) - { - Scratch new_locals{ string_scratch, local_identifiers }; - Scratch new_params{ object_scratch }; - - // collect all locals - const auto params = get_list(values[first_index + 1], str("malformed do expression")); - if (!params) { return params.error(); } - - for (const auto ¶m : values[*params]) { - const auto param_list = get_list(param, str("malformed do expression"), 2); - if (!param_list) { return params.error(); } - - auto id = get_if(&values[(*param_list)[0]]); - if (id == nullptr) { return make_error(str("malformed do expression"), list); } - new_locals.push_back(id->value); - } - - for (const auto ¶m : values[*params]) { - const auto param_list = get_list(param, str("malformed do expression"), 2); - if (!param_list) { return params.error(); } - - std::array new_param{ values[(*param_list)[0]], - fix_identifiers(values[(*param_list)[1]], local_identifiers, local_constants) }; - - // increment thingy (optional) - if (param_list->size == 3) { - new_param[2] = (fix_identifiers(values[(*param_list)[2]], new_locals, local_constants)); - } - new_params.push_back( - SExpr{ values.insert_or_find(std::span{ new_param.begin(), param_list->size == 3u ? 3u : 2u }) }); - } - - Scratch new_do{ object_scratch }; - - // fixup pointer to "do" function - new_do.push_back(fix_identifiers(values[first_index], new_locals, local_constants)); - - // add parameter setup - new_do.push_back(SExpr{ values.insert_or_find(new_params) }); - - for (auto value : values[list.sublist(2)]) { - new_do.push_back(fix_identifiers(value, new_locals, local_constants)); - } - - return SExpr{ values.insert_or_find(new_do) }; - } [[nodiscard]] constexpr SExpr fix_let_identifiers(list_type list, size_type first_index, @@ -1012,7 +1088,7 @@ struct cons_expr auto *id = get_if(&values[(*param_list)[0]]); if (id == nullptr) { return make_error(str("malformed let expression"), list); } - new_locals.push_back(id->value); + new_locals.push_back(to_string(*id)); std::array new_param{ values[(*param_list)[0]], fix_identifiers(values[(*param_list)[1]], local_identifiers, local_constants) }; @@ -1041,7 +1117,7 @@ struct cons_expr const auto *id = get_if(&values[static_cast(first_index + 1)]); if (id == nullptr) { return make_error(str("malformed define expression"), values[first_index + 1]); } - new_locals.push_back(id->value); + new_locals.push_back(to_string(*id)); std::array new_define{ fix_identifiers(values[first_index], local_identifiers, local_constants), values[first_index + 1], @@ -1066,7 +1142,20 @@ struct cons_expr new_lambda.push_back(fix_identifiers(values[index], new_locals, local_constants)); } - return SExpr{ values.insert_or_find(new_lambda) }; + // Create a basic lambda without self-reference + auto result = SExpr{ values.insert_or_find(new_lambda) }; + + // If this is part of a closure with self-reference, preserve that property + if (auto *closure = get_if(&values[list.start]); closure != nullptr && closure->has_self_reference()) { + auto new_closure = Closure{ + closure->parameter_names, + values.insert_or_find(new_lambda), + closure->self_identifier// maintain self-reference identifier + }; + return SExpr{ new_closure }; + } + + return result; } [[nodiscard]] constexpr SExpr @@ -1078,7 +1167,9 @@ struct cons_expr const auto &elem = values[first_index]; string_view_type id; auto fp_type = FunctionPtr::Type::other; - if (auto *id_atom = get_if(&elem); id_atom != nullptr) { id = strings.view(id_atom->value); } + if (auto *id_atom = get_if(&elem); id_atom != nullptr) { + id = strings.view(to_string(*id_atom)); + } if (auto *fp = get_if(&elem); fp != nullptr) { fp_type = fp->type; } if (fp_type == FunctionPtr::Type::lambda_expr || id == str("lambda")) { @@ -1087,8 +1178,6 @@ struct cons_expr return fix_let_identifiers(*list, first_index, local_identifiers, local_constants); } else if (fp_type == FunctionPtr::Type::define_expr || id == str("define")) { return fix_define_identifiers(first_index, local_identifiers, local_constants); - } else if (fp_type == FunctionPtr::Type::do_expr || id == str("do")) { - return fix_do_identifiers(*list, first_index, local_identifiers, local_constants); } } @@ -1101,11 +1190,11 @@ struct cons_expr } else if (auto *id = get_if(&input); id != nullptr) { for (const auto &local : local_identifiers | std::views::reverse) { // do something smarter later, but abort for now because it's in the variable scope - if (local == id->value) { return input; } + if (local == to_string(*id)) { return input; } } for (const auto &object : local_constants | std::views::reverse) { - if (object.first == id->value) { return object.second; } + if (object.first == to_string(*id)) { return object.second; } } return input; @@ -1143,90 +1232,13 @@ struct cons_expr auto variable_id = engine.eval_to(scope, (*variable_elements)[0]); if (!variable_id) { return engine.make_error(str("expected identifier"), variable_id.error()); } - new_scope.emplace_back(variable_id->value, engine.eval(scope, (*variable_elements)[1])); + new_scope.emplace_back(to_string(*variable_id), engine.eval(scope, (*variable_elements)[1])); } // evaluate body return engine.sequence(new_scope, params.sublist(1)); } - - [[nodiscard]] static constexpr SExpr doer(cons_expr &engine, LexicalScope &scope, list_type params) - { - if (params.size < 2) { - return engine.make_error( - str("(do ((var1 val1 [iter_expr1]) ...) (terminate_condition [result...]) [body...])"), params); - } - - Scratch variables{ engine.variables_scratch }; - - auto *variable_list = engine.get_if(&engine.values[params[0]]); - - if (variable_list == nullptr) { - return engine.make_error(str("((var1 val1 [iter_expr1]) ...)"), engine.values[params[0]]); - } - - auto new_scope = scope; - - for (const auto &variable : engine.values[*variable_list]) { - auto *variable_parts = engine.get_if(&variable); - if (variable_parts == nullptr || variable_parts->size < 2 || variable_parts->size > 3) { - return engine.make_error(str("(var1 val1 [iter_expr1])"), variable); - } - - auto variable_parts_list = engine.values[*variable_parts]; - - const auto index = new_scope.size(); - const auto id = engine.eval_to(scope, variable_parts_list[0]); - - if (!id) { return engine.make_error(str("identifier"), id.error()); } - - // initial value - new_scope.emplace_back(id->value, engine.eval(scope, variable_parts_list[1])); - - // increment expression - if (variable_parts->size == 3) { variables.emplace_back(index, variable_parts_list[2]); } - } - - Scratch variable_names{ engine.string_scratch }; - for (auto &[index, value] : variables) { value = engine.fix_identifiers(value, variable_names, scope); } - - for (const auto &local : new_scope) { variable_names.push_back(local.first); } - - const auto terminator_param = engine.values[params[1]]; - const auto *terminator_list = engine.get_if(&terminator_param); - if (terminator_list == nullptr || terminator_list->size == 0) { - return engine.make_error(str("(terminator_condition [result...])"), terminator_param); - } - const auto terminators = engine.values[*terminator_list]; - - auto fixed_up_terminator = engine.fix_identifiers(terminators[0], variable_names, scope); - - // continue while terminator test is false - - bool end = false; - while (!end) { - const auto condition = engine.eval_to(new_scope, fixed_up_terminator); - if (!condition) { return engine.make_error(str("boolean condition"), condition.error()); } - end = *condition; - if (!end) { - // evaluate body - [[maybe_unused]] const auto result = engine.sequence(new_scope, params.sublist(2)); - - Scratch new_values{ engine.variables_scratch }; - - // iterate loop variables - for (const auto &[index, expr] : variables) { new_values.emplace_back(index, engine.eval(new_scope, expr)); } - - // update values - for (auto &[index, value] : new_values) { new_scope[index].second = value; } - } - } - - // evaluate sequence of termination expressions - return engine.sequence(new_scope, terminator_list->sublist(1)); - } - template [[nodiscard]] constexpr std::expected eval_to(LexicalScope &scope, list_type params, string_view_type expected) @@ -1267,6 +1279,8 @@ struct cons_expr return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; } + // (cons 1 '(2 3)) -> '(1 2 3) + // (cons '(a) '(b c)) -> '((a) b c) [[nodiscard]] static constexpr SExpr cons(cons_expr &engine, LexicalScope &scope, list_type params) { auto evaled_params = engine.eval_to(scope, params, str("(cons Expr LiteralList)")); @@ -1276,47 +1290,56 @@ struct cons_expr Scratch result{ engine.object_scratch }; if (const auto *list_front = std::get_if(&front.value); list_front != nullptr) { + // First element is a list, add it as a nested list result.push_back(SExpr{ list_front->items }); } else if (const auto *atom = std::get_if(&front.value); atom != nullptr) { if (const auto *identifier_front = std::get_if(atom); identifier_front != nullptr) { - // push an identifier into the list, not a symbol... should maybe fix this - // so quoted lists are always lists of symbols? - result.push_back(SExpr{ Atom{ identifier_type{ identifier_front->value } } }); + // Convert symbol to identifier when adding to result list + // Note: should maybe fix this so quoted lists are always lists of symbols? + result.push_back(SExpr{ Atom{ to_identifier(*identifier_front) } }); } else { + // Regular atom, keep as-is result.push_back(front); } } else { + // Any other expression type result.push_back(front); } + // Add the remaining elements from the second list for (const auto &value : engine.values[list.items]) { result.push_back(value); } return SExpr{ LiteralList{ engine.values.insert_or_find(result) } }; } + // Helper for monadic-style error handling + // If operation succeeded, calls callable with the result + // If operation failed, propagates the error template [[nodiscard]] static constexpr SExpr error_or_else(const std::expected &obj, auto callable) { - if (obj) { - return callable(*obj); - } else { - return obj.error(); - } + if (obj) { return callable(*obj); } + return obj.error(); } + // (cdr '(1 2 3)) -> '(2 3) + // (cdr '(1)) -> '() + // (cdr '()) -> ERROR [[nodiscard]] static constexpr SExpr cdr(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else( engine.eval_to(scope, params, str("(cdr LiteralList)")), [&](const auto &list) { - // If the list has one or zero elements, return empty list - if (list.items.size <= 1) { - static constexpr IndexedList empty_list{ 0, 0 }; - return SExpr{ literal_list_type{ empty_list } }; - } + // Check if the list is empty + if (list.items.size == 0) { return engine.make_error(str("cdr: cannot take cdr of empty list"), params); } + // If the list has one element, return empty list + if (list.items.size == 1) { return SExpr{ literal_list_type{ empty_indexed_list } }; } return SExpr{ list.sublist(1) }; }); } + // (car '(1 2 3)) -> 1 + // (car '((a b) c)) -> '(a b) + // (car '()) -> ERROR [[nodiscard]] static constexpr SExpr car(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else( @@ -1332,6 +1355,12 @@ struct cons_expr return SExpr{ literal_list_type{ *nested_list } }; } + if (const auto *atom = std::get_if(&elem.value); atom != nullptr) { + if (const auto *identifier = std::get_if(atom); identifier != nullptr) { + return SExpr{ Atom{ symbol_type{ to_symbol(*identifier) } } }; + } + } + return elem; }); } @@ -1344,25 +1373,67 @@ struct cons_expr }); } + [[nodiscard]] static constexpr SExpr begin(cons_expr &engine, LexicalScope &scope, list_type params) + { + return engine.sequence(scope, params); + } + + [[nodiscard]] static constexpr SExpr evaler(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else(engine.eval_to(scope, params, str("(eval LiteralList)")), [&](const auto &list) { return engine.eval(engine.global_scope, SExpr{ list.items }); }); } + // (cond ((< 5 10) "less") ((> 5 10) "greater") (else "equal")) -> "less" + // (cond ((= 5 10) "equal") ((> 5 10) "greater") (else "less")) -> "less" + [[nodiscard]] static constexpr SExpr cond(cons_expr &engine, LexicalScope &scope, list_type params) + { + // Evaluate each condition pair in sequence + for (const auto &entry : engine.values[params]) { + const auto cond = engine.eval_to(scope, entry); + if (!cond) { return engine.make_error(str("(condition statement)"), cond.error()); } + if (cond->size != 2) { + return engine.make_error(str("(condition statement) requires both condition and result"), entry); + } + + // Check for the special 'else' case - always matches and returns its expression + if (const auto *cond_str = get_if(&engine.values[(*cond)[0]]); + cond_str != nullptr && engine.strings.view(to_string(*cond_str)) == str("else")) { + // we've reached the "else" condition + return engine.eval(scope, engine.values[(*cond)[1]]); + } else { + // Evaluate the condition to check if it's true + const auto condition = engine.eval_to(scope, engine.values[(*cond)[0]]); + if (!condition) { return engine.make_error(str("boolean condition"), condition.error()); } + + // If this condition matches, evaluate and return its expression + if (*condition) { return engine.eval(scope, engine.values[(*cond)[1]]); } + } + } + + // No matching condition, including no else clause + return engine.make_error(str("No matching condition found"), params); + } + + + // (if true 1 2) -> 1 + // (if false 1 2) -> 2 + // (if (< 5 10) (+ 1 2) (- 10 5)) -> 3 [[nodiscard]] static constexpr SExpr ifer(cons_expr &engine, LexicalScope &scope, list_type params) { // need to be careful to not execute unexecuted branches if (params.size != 3) { return engine.make_error(str("(if bool-cond then else)"), params); } + // Evaluate the condition to a boolean const auto condition = engine.eval_to(scope, engine.values[params[0]]); - if (!condition) { return engine.make_error(str("boolean condition"), condition.error()); } + // Only evaluate the branch that needs to be taken if (*condition) { - return engine.eval(scope, engine.values[params[1]]); + return engine.eval(scope, engine.values[params[1]]);// true branch } else { - return engine.eval(scope, engine.values[params[2]]); + return engine.eval(scope, engine.values[params[2]]);// false branch } } @@ -1379,7 +1450,37 @@ struct cons_expr return SExpr{ Atom{ std::monostate{} } }; } - [[nodiscard]] static constexpr SExpr quoter(cons_expr &engine, LexicalScope &, list_type params) + // error?: Check if the expression is an error + [[nodiscard]] static constexpr SExpr error_p(cons_expr &engine, LexicalScope &scope, list_type params) + { + if (params.size != 1) { return engine.make_error(str("(error? expr)"), params); } + + // Evaluate the expression + auto expr = engine.eval(scope, engine.values[params[0]]); + + // Check if it's an error type + const bool is_error = std::holds_alternative(expr.value); + + return SExpr{ Atom(is_error) }; + } + + // Generic type predicate template for any type(s) + template [[nodiscard]] static constexpr function_ptr make_type_predicate() + { + return [](cons_expr &engine, LexicalScope &scope, list_type params) -> SExpr { + if (params.size != 1) { return engine.make_error(str("(type? expr)"), params); } + + // Evaluate the expression + auto expr = engine.eval(scope, engine.values[params[0]]); + + // Use fold expression with get_if to check if any of the specified types match + bool is_type = ((get_if(&expr) != nullptr) || ...); + + return SExpr{ Atom(is_type) }; + }; + } + + [[nodiscard]] static constexpr SExpr quote(cons_expr &engine, list_type params) { if (params.size != 1) { return engine.make_error(str("(quote expr)"), params); } @@ -1388,16 +1489,13 @@ struct cons_expr // If it's a list, convert it to a literal list if (const auto *list = std::get_if(&expr.value); list != nullptr) { // Special case for empty lists - use a canonical empty list with start index 0 - if (list->size == 0) { - static constexpr IndexedList empty_list{ 0, 0 }; - return SExpr{ literal_list_type{ empty_list } }; - } + if (list->size == 0) { return SExpr{ literal_list_type{ empty_indexed_list } }; } return SExpr{ literal_list_type{ *list } }; } // If it's an identifier, convert it to a symbol else if (const auto *atom = std::get_if(&expr.value); atom != nullptr) { if (const auto *id = std::get_if(atom); id != nullptr) { - return SExpr{ Atom{ symbol_type{ id->value } } }; + return SExpr{ Atom{ symbol_type{ to_symbol(*id) } } }; } } @@ -1405,11 +1503,32 @@ struct cons_expr return expr; } + [[nodiscard]] static constexpr SExpr quoter(cons_expr &engine, LexicalScope &, list_type params) + { + return quote(engine, params); + } + [[nodiscard]] static constexpr SExpr definer(cons_expr &engine, LexicalScope &scope, list_type params) { return error_or_else(engine.eval_to(scope, params, str("(define Identifier Expression)")), [&](const auto &evaled) { - scope.emplace_back(std::get<0>(evaled).value, engine.fix_identifiers(std::get<1>(evaled), {}, scope)); + const auto &identifier = std::get<0>(evaled); + auto expr = std::get<1>(evaled); + + // Check if the expression is a lambda (closure) + if (auto *closure_ptr = std::get_if(&expr.value); closure_ptr != nullptr) { + // Create a mutable copy of the closure + Closure closure = *closure_ptr; + + // Set up self-reference for recursion + closure.self_identifier = identifier; + + // Update the expression with the modified closure + expr = SExpr{ closure }; + } + + // Fix identifiers and add to scope + scope.emplace_back(to_string(identifier), engine.fix_identifiers(expr, {}, scope)); return SExpr{ Atom{ std::monostate{} } }; }); } @@ -1420,7 +1539,7 @@ struct cons_expr [[nodiscard]] constexpr auto make_callable(SExpr callable) noexcept requires std::is_function_v { - auto impl = [this, callable](Ret (*)(Params...)) { + auto impl = [callable](Ret (*)(Params...)) { return [callable](cons_expr &engine, Params... params) { std::array args{ SExpr{ Atom{ params } }... }; if constexpr (std::is_same_v) { @@ -1441,7 +1560,7 @@ struct cons_expr requires std::is_function_v { // this is fragile, we need to check parsing better - return make_callable(eval(global_scope, values[std::get(parse(function).first.value)][0])); + return make_callable(eval(global_scope, values[parse(function).first][0])); } @@ -1483,20 +1602,20 @@ struct cons_expr { for (const auto &next : engine.values[params] | engine.eval_transform(scope)) { if (!next) { return engine.make_error(str("parameter not boolean"), next.error()); } - if (!(*next)) { return SExpr{ Atom{ false } }; } + if (!(*next)) { return False; } } - return SExpr{ Atom{ true } }; + return True; } [[nodiscard]] static constexpr SExpr logical_or(cons_expr &engine, LexicalScope &scope, list_type params) { for (const auto &next : engine.values[params] | engine.eval_transform(scope)) { if (!next) { return engine.make_error(str("parameter not boolean"), next.error()); } - if (*next) { return SExpr{ Atom{ true } }; } + if (*next) { return True; } } - return SExpr{ Atom{ false } }; + return False; } template @@ -1509,10 +1628,10 @@ struct cons_expr const auto &result = engine.eval_to(scope, elem); if (!result) { return engine.make_error(str("same types for operator"), SExpr{ next }, result.error()); } const auto prev = std::exchange(next, *result); - if (!Op(prev, next)) { return SExpr{ Atom{ false } }; } + if (!Op(prev, next)) { return False; } } - return SExpr{ Atom{ true } }; + return True; } else { return engine.make_error(str("supported types"), params); } @@ -1521,22 +1640,16 @@ struct cons_expr if (params.size < 2) { return engine.make_error(str("at least 2 parameters"), params); } auto first_param = engine.eval(scope, engine.values[params[0]]).value; - // For working directly on "LiteralList" objects if (const auto *list = std::get_if(&first_param); list != nullptr) { return sum(*list); } + if (const auto *closure = std::get_if(&first_param); closure != nullptr) { return sum(*closure); } if (const auto *atom = std::get_if(&first_param); atom != nullptr) { return visit(sum, *atom); } + return engine.make_error(str("supported types"), params); } - [[nodiscard]] constexpr SExpr evaluate(string_view_type input) - { - const auto result = parse(input).first; - const auto *list = std::get_if(&result.value); - - if (list != nullptr) { return sequence(global_scope, *list); } - return result; - } + [[nodiscard]] constexpr SExpr evaluate(string_view_type input) { return sequence(global_scope, parse(input).first); } template [[nodiscard]] constexpr std::expected evaluate_to(string_view_type input) { diff --git a/include/cons_expr/utility.hpp b/include/cons_expr/utility.hpp index 09736af..dbede30 100644 --- a/include/cons_expr/utility.hpp +++ b/include/cons_expr/utility.hpp @@ -11,7 +11,7 @@ template inline constexpr bool is_cons_expr_v = false; template; template std::string to_string(const Eval &, bool annotate, const typename Eval::SExpr &input); template std::string to_string(const Eval &, bool annotate, const bool input); -template std::string to_string(const Eval &, bool annotate, const typename Eval::float_type input); +template std::string to_string(const Eval &, bool annotate, const typename Eval::real_type input); template std::string to_string(const Eval &, bool annotate, const typename Eval::int_type input); template std::string to_string(const Eval &, bool annotate, const typename Eval::Closure &); template std::string to_string(const Eval &, bool annotate, const std::monostate &); @@ -71,9 +71,19 @@ template std::string to_string(const Eval &engine, bool annotate, const typename Eval::identifier_type &id) { if (annotate) { - return std::format("[identifier] {{{}, {}}} {}", id.value.start, id.value.size, engine.strings.view(id.value)); + return std::format("[identifier] {{{}, {}}} {}", id.start, id.size, engine.strings.view(to_string(id))); } else { - return std::string{ engine.strings.view(id.value) }; + return std::string{ engine.strings.view(to_string(id)) }; + } +} + + +template std::string to_string(const Eval &engine, bool annotate, const typename Eval::symbol_type &id) +{ + if (annotate) { + return std::format("[symbol] {{{}, {}}} '{}", id.start, id.size, engine.strings.view(to_string(id))); + } else { + return std::format("'{}", engine.strings.view(to_string(id))); } } @@ -94,10 +104,10 @@ template std::string to_string(const Eval &engine, bool annotate, return std::visit([&](const auto &value) { return to_string(engine, annotate, value); }, input); } -template std::string to_string(const Eval &, bool annotate, const typename Eval::float_type input) +template std::string to_string(const Eval &, bool annotate, const typename Eval::real_type input) { std::string result; - if (annotate) { result = "[floating_point] "; } + if (annotate) { result = "[real] "; } return result + std::format("{}", input); } @@ -144,7 +154,7 @@ template std::string to_string(const Eval &engine, bool annotate, const typename Eval::string_type &string) { if (annotate) { - return std::format("[identifier] {{{}, {}}} \"{}\"", string.start, string.size, engine.strings.view(string)); + return std::format("[string] {{{}, {}}} \"{}\"", string.start, string.size, engine.strings.view(string)); } else { return std::format("\"{}\"", engine.strings.view(string)); } @@ -156,4 +166,4 @@ template std::string to_string(const Eval &engine, bool annotate, } }// namespace lefticus -#endif \ No newline at end of file +#endif diff --git a/src/ccons_expr/CMakeLists.txt b/src/ccons_expr/CMakeLists.txt index 109f485..462ec29 100644 --- a/src/ccons_expr/CMakeLists.txt +++ b/src/ccons_expr/CMakeLists.txt @@ -15,4 +15,13 @@ target_link_system_libraries( ftxui::dom ftxui::component) -target_include_directories(ccons_expr PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") \ No newline at end of file +target_include_directories(ccons_expr PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") + + +if(EMSCRIPTEN) + cons_expr_configure_wasm_target(ccons_expr + TITLE "ccons_expr" + DESCRIPTION "FTXUI Front-End to cons_expr" + IO_MODE "FTXUI" + ) +endif() diff --git a/src/ccons_expr/main.cpp b/src/ccons_expr/main.cpp index 8bd30d4..148cd1d 100644 --- a/src/ccons_expr/main.cpp +++ b/src/ccons_expr/main.cpp @@ -1,19 +1,22 @@ +#include +#include #include #include #include -#include "ftxui/component/captured_mouse.hpp"// for ftxui #include "ftxui/component/component.hpp"// for Input, Renderer, ResizableSplitLeft #include "ftxui/component/component_base.hpp"// for ComponentBase, Component #include "ftxui/component/screen_interactive.hpp"// for ScreenInteractive #include "ftxui/dom/elements.hpp"// for operator|, separator, text, Element, flex, vbox, border -#include "ftxui/dom/table.hpp"// for operator|, separator, text, Element, flex, vbox, border #include #include #include +static constexpr int InitialSplitWidth = 50; +static constexpr int GlobalsHeight = 5; +static constexpr int ValuesHeight = 7; int main([[maybe_unused]] int argc, [[maybe_unused]] const char *argv[]) { @@ -38,21 +41,20 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char *argv[]) auto update_objects = [&]() { entries.clear(); - for (std::size_t index = 0; auto item : evaluator.values[{ 0, evaluator.values.size() }]) { + for (std::size_t index = 0; auto item : evaluator.values[{ .start = 0, .size = evaluator.values.size() }]) { entries.push_back(std::format("{}: {}", index, to_string(evaluator, true, item))); ++index; } characters.clear(); - for (std::size_t index = 0; auto item : evaluator.strings[{ 0, evaluator.strings.size() }]) { + for (std::size_t index = 0; auto item : evaluator.strings[{ .start = 0, .size = evaluator.strings.size() }]) { characters.push_back(std::format("{}: '{}'", index, item)); ++index; } globals.clear(); - for (std::size_t index = 0; auto [key, value] : evaluator.global_scope[{ 0, evaluator.global_scope.size() }]) { + for (auto [key, value] : evaluator.global_scope[{ .start = 0, .size = evaluator.global_scope.size() }]) { globals.push_back(std::format("{}: '{}'", to_string(evaluator, false, key), to_string(evaluator, true, value))); - ++index; } }; @@ -66,10 +68,8 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char *argv[]) try { - content_2 += to_string(evaluator, - true, - evaluator.sequence( - evaluator.global_scope, std::get::list_type>(evaluator.parse(content_1).first.value))); + content_2 += + to_string(evaluator, true, evaluator.sequence(evaluator.global_scope, evaluator.parse(content_1).first)); } catch (const std::exception &e) { content_2 += std::string("Error: ") + e.what(); } @@ -80,7 +80,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char *argv[]) auto button = ftxui::Button("Evaluate", do_evaluate); - int size = 50; + int size = InitialSplitWidth; auto resizeable_bits = ftxui::ResizableSplitLeft(textarea_1, output_1, &size); auto radiobox = ftxui::Menu(&entries, &selected); @@ -116,10 +116,10 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char *argv[]) return ftxui::hbox({ characterbox->Render() | ftxui::vscroll_indicator | ftxui::frame, ftxui::separator(), ftxui::vbox({ globalsbox->Render() | ftxui::vscroll_indicator | ftxui::frame - | ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, 5), + | ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, GlobalsHeight), ftxui::separator(), radiobox->Render() | ftxui::vscroll_indicator | ftxui::frame - | ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, 7), + | ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, ValuesHeight), ftxui::separator(), resizeable_bits->Render() | ftxui::flex, ftxui::separator(), diff --git a/src/cons_expr_cli/CMakeLists.txt b/src/cons_expr_cli/CMakeLists.txt index 0e1acb7..4dbf45c 100644 --- a/src/cons_expr_cli/CMakeLists.txt +++ b/src/cons_expr_cli/CMakeLists.txt @@ -13,3 +13,11 @@ target_link_system_libraries( target_include_directories(cons_expr_cli PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") set_target_properties(cons_expr_cli PROPERTIES OUTPUT_NAME "cons_expr") + +if(EMSCRIPTEN) + cons_expr_configure_wasm_target(cons_expr_cli + TITLE "cons_expr_cli" + DESCRIPTION "regular command-line front end to cons_expr" + IO_MODE "CONSOLE" + ) +endif() diff --git a/src/cons_expr_cli/main.cpp b/src/cons_expr_cli/main.cpp index a7874e7..911d3f6 100644 --- a/src/cons_expr_cli/main.cpp +++ b/src/cons_expr_cli/main.cpp @@ -1,17 +1,43 @@ - #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include #include #include #include +#include +#include using cons_expr_type = lefticus::cons_expr<>; +namespace fs = std::filesystem; + +namespace { +void display(cons_expr_type::int_type value) { std::cout << value << '\n'; } -void display(cons_expr_type::int_type i) { std::cout << i << '\n'; } +// Read a file into a string +std::string read_file(const fs::path &path) +{ + if (!fs::exists(path)) { throw std::runtime_error(std::format("File not found: {}", path.string())); } + std::ifstream const file(path, std::ios::in | std::ios::binary); + if (!file) { throw std::runtime_error(std::format("Failed to open file: {}", path.string())); } + + std::stringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); +} +}// namespace int main(int argc, const char **argv) { @@ -19,9 +45,11 @@ int main(int argc, const char **argv) CLI::App app{ std::format("{} version {}", cons_expr::cmake::project_name, cons_expr::cmake::project_version) }; std::optional script; + std::optional file_path; bool show_version = false; app.add_flag("--version", show_version, "Show version information"); - app.add_option("--exec", script, "Script to execute"); + app.add_option("--exec", script, "Script to execute directly"); + app.add_option("--file", file_path, "File containing script to execute"); CLI11_PARSE(app, argc, argv); @@ -34,14 +62,53 @@ int main(int argc, const char **argv) evaluator.add("display"); + // Process script from command line if (script) { - std::cout << lefticus::to_string(evaluator, - false, - evaluator.sequence( - evaluator.global_scope, std::get(evaluator.parse(*script).first.value))); + std::cout << "Executing script from command line...\n"; + std::cout << lefticus::to_string( + evaluator, false, evaluator.sequence(evaluator.global_scope, evaluator.parse(*script).first)); std::cout << '\n'; } + + // Process script from file + if (file_path) { + try { + std::cout << "Executing script from file: " << *file_path << '\n'; + std::string const file_content = read_file(fs::path(*file_path)); + + auto [parse_result, remaining] = evaluator.parse(file_content); + auto result = evaluator.sequence(evaluator.global_scope, parse_result); + + std::cout << "Result: " << lefticus::to_string(evaluator, false, result) << '\n'; + } catch (const std::exception &e) { + spdlog::error("Error processing file '{}': {}", *file_path, e.what()); + return EXIT_FAILURE; + } + } + + // If no script or file provided, display usage + if (!script && !file_path) { + + while (true) { + std::cout << "cons_expr> " << std::flush; + + std::string line; + std::getline(std::cin, line); + + if (!std::cin.good()) { break; } + + + auto [parse_result, remaining] = evaluator.parse(line); + auto result = evaluator.sequence(evaluator.global_scope, parse_result); + + std::cout << lefticus::to_string(evaluator, false, result) << '\n'; + } + } + } catch (const std::exception &e) { spdlog::error("Unhandled exception in main: {}", e.what()); + return EXIT_FAILURE; } + + return EXIT_SUCCESS; } diff --git a/test/.clang-tidy b/test/.clang-tidy new file mode 100644 index 0000000..90b17ab --- /dev/null +++ b/test/.clang-tidy @@ -0,0 +1,8 @@ +--- +# Inherit configuration from parent directory +Checks: " + -readability-function-cognitive-complexity, + -readability-magic-numbers, + -cppcoreguidelines-avoid-magic-numbers + " +InheritParentConfig: true diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 289e629..3f7db6c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,20 @@ add_test(NAME cli.has_help COMMAND cons_expr_cli --help) add_test(NAME cli.version_matches COMMAND cons_expr_cli --version) set_tests_properties(cli.version_matches PROPERTIES PASS_REGULAR_EXPRESSION "${PROJECT_VERSION}") +# Test direct script execution +add_test(NAME cli.direct_script COMMAND cons_expr_cli --exec "(+ 1 2)") +set_tests_properties(cli.direct_script PROPERTIES PASS_REGULAR_EXPRESSION "3") + +# Test file input handling with a simple script +add_test(NAME cli.file_input COMMAND cons_expr_cli --file "${CMAKE_CURRENT_SOURCE_DIR}/test_script.scm") +set_tests_properties(cli.file_input PROPERTIES PASS_REGULAR_EXPRESSION "30") + +# Test file input with a non-existent file +add_test(NAME cli.missing_file COMMAND cons_expr_cli --file "non_existent_file.scm") +set_tests_properties(cli.missing_file PROPERTIES + WILL_FAIL TRUE + FAIL_REGULAR_EXPRESSION "File not found") + add_executable(tests tests.cpp) target_link_libraries( tests @@ -65,7 +79,17 @@ catch_discover_tests( .xml) # Add a file containing a set of constexpr tests -add_executable(constexpr_tests constexpr_tests.cpp list_tests.cpp parser_tests.cpp recursion_tests.cpp) +add_executable(constexpr_tests constexpr_tests.cpp + list_tests.cpp + parser_tests.cpp + recursion_tests.cpp + scoping_tests.cpp + type_predicate_tests.cpp + string_escape_tests.cpp + error_handling_tests.cpp + recursion_and_closure_tests.cpp + cond_tests.cpp + list_construction_tests.cpp) target_link_libraries( constexpr_tests PRIVATE cons_expr::cons_expr @@ -93,7 +117,17 @@ catch_discover_tests( # Disable the constexpr portion of the test, and build again this allows us to have an executable that we can debug when # things go wrong with the constexpr testing -add_executable(relaxed_constexpr_tests constexpr_tests.cpp list_tests.cpp parser_tests.cpp recursion_tests.cpp) +add_executable(relaxed_constexpr_tests constexpr_tests.cpp + list_tests.cpp + parser_tests.cpp + recursion_tests.cpp + scoping_tests.cpp + type_predicate_tests.cpp + string_escape_tests.cpp + error_handling_tests.cpp + recursion_and_closure_tests.cpp + cond_tests.cpp + list_construction_tests.cpp) target_link_libraries( relaxed_constexpr_tests PRIVATE cons_expr::cons_expr diff --git a/test/cond_tests.cpp b/test/cond_tests.cpp new file mode 100644 index 0000000..7b82aae --- /dev/null +++ b/test/cond_tests.cpp @@ -0,0 +1,167 @@ +#include + +#include +#include +#include +#include + +using IntType = int; +using FloatType = double; + +namespace { +template constexpr Result evaluate_to(std::string_view input) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value(); +} + +template constexpr bool evaluate_expected(std::string_view input, auto result) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; +} + + +// Helper to check if an expression results in an error +constexpr bool is_error(std::string_view input) +{ + lefticus::cons_expr evaluator; + auto result = evaluator.evaluate(input); + return std::holds_alternative>(result.value); +} +}// namespace + +TEST_CASE("Cond expression basic usage", "[cond]") +{ + // Basic cond with one matching clause + STATIC_CHECK(evaluate_to(R"( + (cond + ((< 5 10) 1) + (else 2)) + )") == 1); + + // Basic cond with else clause + STATIC_CHECK(evaluate_to(R"( + (cond + ((> 5 10) 1) + (else 2)) + )") == 2); + + // Cond with multiple conditions + STATIC_CHECK(evaluate_to(R"( + (cond + ((> 5 10) 1) + ((< 5 10) 2) + (else 3)) + )") == 2); + + // Cond with multiple conditions, evaluating last one + STATIC_CHECK(evaluate_to(R"( + (cond + ((> 5 10) 1) + ((> 5 20) 2) + (else 3)) + )") == 3); +} + +TEST_CASE("Cond with complex expressions", "[cond]") +{ + // Cond with expressions in conditions + STATIC_CHECK(evaluate_to(R"( + (cond + ((< (+ 2 3) (* 2 3)) 1) + ((> (+ 2 3) (* 2 3)) 2) + (else 3)) + )") == 1); + + // Cond with expressions in results + STATIC_CHECK(evaluate_to(R"( + (cond + ((< 5 10) (+ 1 2)) + (else (- 10 5))) + )") == 3); + + // Nested cond expressions + STATIC_CHECK(evaluate_to(R"( + (cond + ((< 5 10) (cond + ((> 3 1) 1) + (else 2))) + (else 3)) + )") == 1); +} + +TEST_CASE("Cond without else clause", "[cond]") +{ + // Cond with multiple conditions but no else, with a match + STATIC_CHECK(evaluate_to(R"( + (cond + ((> 5 10) 1) + ((< 5 10) 2)) + )") == 2); + + // Cond with no else and no matching condition should error + STATIC_CHECK(is_error(R"( + (cond + ((> 5 10) 1) + ((> 5 20) 2)) + )")); +} + +TEST_CASE("Cond with side effects", "[cond]") +{ + // Only the matching condition's result should be evaluated + STATIC_CHECK(evaluate_to(R"( + (define x 5) + (define y 10) + (cond + ((< x y) x) + (else (/ x 0))) ; This would error if evaluated + )") == 5); + + // Similarly, condition expressions should be evaluated in sequence + STATIC_CHECK(evaluate_to(R"( + (cond + ((< 5 10) 1) + ((/ 1 0) 2)) ; This division by zero should not occur + )") == 1); +} + +TEST_CASE("Cond with boolean conditions", "[cond]") +{ + // Directly using boolean values + STATIC_CHECK(evaluate_to(R"( + (cond + (true 1) + (else 2)) + )") == 1); + + STATIC_CHECK(evaluate_to(R"( + (cond + (false 1) + (else 2)) + )") == 2); + + // Using boolean expressions + STATIC_CHECK(evaluate_to(R"( + (cond + ((and (< 5 10) (> 5 1)) 1) + (else 2)) + )") == 1); +} + +TEST_CASE("Cond error handling", "[cond][error]") +{ + // Malformed cond syntax + STATIC_CHECK(is_error("(cond)")); + STATIC_CHECK(is_error("(cond 1 2 3)")); + + // Condition clause not a list + STATIC_CHECK(is_error("(cond 42 else)")); + + // Condition clause without result + STATIC_CHECK(is_error("(cond ((< 5 10)))")); + + // Non-boolean condition (should be okay actually) + // STATIC_CHECK(evaluate_to("(cond (1 42) (else 0))") == 42); +} diff --git a/test/constexpr_tests.cpp b/test/constexpr_tests.cpp index 58d3be0..3ac2e09 100644 --- a/test/constexpr_tests.cpp +++ b/test/constexpr_tests.cpp @@ -1,9 +1,17 @@ +#include #include -#include #include #include +#include +#include +#include #include +#include +#include +#include +#include +#include using IntType = int; using FloatType = double; @@ -12,7 +20,7 @@ static_assert(lefticus::is_cons_expr_v>); static_assert(std::is_trivially_copyable_v::SExpr>); - +namespace { template constexpr Result evaluate_to(std::string_view input) { lefticus::cons_expr evaluator; @@ -29,15 +37,11 @@ template constexpr bool evaluate_expected(std::string_view inpu template constexpr std::optional parse_as(auto &evaluator, std::string_view input) { - using eval_type = std::remove_cvref_t; - using list_type = eval_type::list_type; - auto [parse_result, parse_remaining] = evaluator.parse(input); // properly parsed results are always lists - const auto list = std::get_if(&parse_result.value); // this should be a list of exactly 1 thing (which might be another list) - if (list == nullptr || list->size != 1) { return std::optional{}; } - const auto first_elem = evaluator.values[(*list)[0]]; + if (parse_result.size != 1) { return std::optional{}; } + const auto first_elem = evaluator.values[parse_result[0]]; const auto *result = evaluator.template get_if(&first_elem); @@ -45,6 +49,19 @@ template constexpr std::optional parse_as(auto &evaluat return *result; } +}// namespace + +TEST_CASE("Literals") +{ + STATIC_CHECK(evaluate_to("1") == 1); + STATIC_CHECK(evaluate_to("1.1") == 1.1); + STATIC_CHECK(evaluate_to("true") == true); + STATIC_CHECK(evaluate_to("false") == false); + + + STATIC_CHECK( + !std::holds_alternative::error_type>(lefticus::cons_expr<>{}.evaluate("42").value)); +} TEST_CASE("Operator identifiers", "[operators]") { @@ -65,6 +82,12 @@ TEST_CASE("basic float operators", "[operators]") STATIC_CHECK(evaluate_to("(/ 10.0 4.0)") == FloatType{ 2.5 }); } +TEST_CASE("mismatched operators", "[operators]") +{ + // validate that we cannot fold over mismatched types + STATIC_CHECK(evaluate_to("(error? (+ 1.0 1))") == true); + STATIC_CHECK(evaluate_to("(error? (+ 1.0))") == true); +} TEST_CASE("basic string_view operators", "[operators]") { @@ -82,6 +105,40 @@ TEST_CASE("access as string_view", "[strings]") STATIC_CHECK(evaluate_expected(R"("")", "")); } +TEST_CASE("string escape character processing", "[strings][escapes]") +{ + // Test escaped double quotes + STATIC_CHECK(evaluate_expected(R"("Quote: \"Hello\"")", "Quote: \"Hello\"")); + + // Test escaped backslash + STATIC_CHECK(evaluate_expected(R"("Backslash: \\")", "Backslash: \\")); + + // Test newline escape + STATIC_CHECK(evaluate_expected(R"("Line1\nLine2")", "Line1\nLine2")); + + // Test tab escape + STATIC_CHECK(evaluate_expected(R"("Tabbed\tText")", "Tabbed\tText")); + + // Test carriage return escape + STATIC_CHECK(evaluate_expected(R"("Return\rText")", "Return\rText")); + + // Test form feed escape + STATIC_CHECK(evaluate_expected(R"("Form\fFeed")", "Form\fFeed")); + + // Test backspace escape + STATIC_CHECK(evaluate_expected(R"("Back\bSpace")", "Back\bSpace")); + + // Test multiple escapes in one string + STATIC_CHECK(evaluate_expected( + R"("Multiple\tEscapes:\n\"Quoted\", \\Backslash")", "Multiple\tEscapes:\n\"Quoted\", \\Backslash")); + + // Test consecutive escapes + STATIC_CHECK(evaluate_expected(R"("Double\\\\Backslash")", "Double\\\\Backslash")); + + // Test escape at end of string + STATIC_CHECK(evaluate_expected(R"("EndEscape\\")", "EndEscape\\")); +} + TEST_CASE("basic integer operators", "[operators]") { STATIC_CHECK(evaluate_to("(+ 1 2)") == 3); @@ -109,6 +166,26 @@ TEST_CASE("list comparisons", "[operators]") STATIC_CHECK(evaluate_to("(!= '(1 2) '(1 2 3))") == true); } +TEST_CASE("unsupported operators", "[operators]") +{ + // sanity check + STATIC_CHECK(evaluate_to("(error? (== 1 1))") == false); + + // functions are not currently comparable + STATIC_CHECK(evaluate_to("(error? (== + +))") == true); + + // functions are not addable + STATIC_CHECK(evaluate_to("(error? (+ + +))") == true); + + // cannot add string to int + STATIC_CHECK(evaluate_to(R"((error? (+ 1 "Hello")))") == true); + + STATIC_CHECK(evaluate_to(R"((error? (+ 1 +)))") == true); + STATIC_CHECK(evaluate_to(R"((error? (+ 1 +)))") == true); + STATIC_CHECK(evaluate_to(R"((error? (+ 'a 'b)))") == true); +} + + TEST_CASE("basic integer comparisons", "[operators]") { STATIC_CHECK(evaluate_to("(== 12 12)") == true); @@ -152,6 +229,10 @@ TEST_CASE("basic lambda usage", "[lambdas]") STATIC_CHECK(evaluate_to("((lambda (x) (* x x)) 11)") == 121); STATIC_CHECK(evaluate_to("((lambda (x y) (+ x y)) 5 7)") == 12); STATIC_CHECK(evaluate_to("((lambda (x y z) (+ x (* y z))) 5 7 2)") == 19); + + // bad lambda parse + STATIC_CHECK(evaluate_to("(error? (lambda ()))") == true); + STATIC_CHECK(evaluate_to("(error? (lambda 1 2))") == true); } TEST_CASE("nested lambda usage", "[lambdas]") @@ -262,26 +343,6 @@ TEST_CASE("GPT Generated Tests", "[integration tests]") (let ((y 3)) (+ x y))) )") == 5); - - // Recursive functions like Fibonacci can't be evaluated at compile-time - // because they require unbounded recursion - /* - STATIC_CHECK(evaluate_to(R"( -(define fib - (lambda (n) - (if (< n 2) - n - (+ (fib (- n 1)) (fib (- n 2)))))) -(fib 6) -)") == 8); - */ - - // Instead we use the `do` construct for iteration, which works well in constexpr - STATIC_CHECK(evaluate_to(R"( -(do ((n 5 (- n 1)) - (result 1 (* result n))) - ((<= n 1) result)) -)") == 120); } TEST_CASE("binary short circuiting", "[short circuiting]") @@ -485,6 +546,13 @@ TEST_CASE("simple append expression", "[builtins]") // Append with evaluated expressions STATIC_CHECK(evaluate_to("(== (append (list (+ 1 2)) (list (* 2 2))) '(3 4))") == true); + + // bad append + STATIC_CHECK(evaluate_to("(error? (append '() '()))") == false); + STATIC_CHECK(evaluate_to("(error? (append 1 '()))") == true); + STATIC_CHECK(evaluate_to("(error? (append 1 1))") == true); + STATIC_CHECK(evaluate_to("(error? (append 1))") == true); + STATIC_CHECK(evaluate_to("(error? (append))") == true); } TEST_CASE("if expressions", "[builtins]") @@ -505,50 +573,11 @@ TEST_CASE("if expressions", "[builtins]") STATIC_CHECK(evaluate_to("(if (> 5 2) (+ 10 5) (* 3 4))") == 15); } -TEST_CASE("do expression", "[builtins]") -{ - STATIC_CHECK(evaluate_to("(do () (true 0))") == 0); - - // Sum numbers from 1 to 10 - STATIC_CHECK(evaluate_to(R"( -(do ((i 1 (+ i 1)) - (sum 0 (+ sum i))) - ((> i 10) sum) -) -)") == 55); - - // Compute factorial of 5 - STATIC_CHECK(evaluate_to(R"( -(do ((n 5 (- n 1)) - (result 1 (* result n))) - ((<= n 1) result)) -)") == 120); - - // Compute the 7th Fibonacci number (0-indexed) - STATIC_CHECK(evaluate_to(R"( -(do ((i 0 (+ i 1)) - (a 0 b) - (b 1 (+ a b))) - ((>= i 7) a)) -)") == 13); - - // Count by twos - STATIC_CHECK(evaluate_to(R"( -(do ((i 0 (+ i 2)) - (count 0 (+ count 1))) - ((>= i 10) count)) -)") == 5); -} TEST_CASE("simple error handling", "[errors]") { evaluate_to::error_type>(R"( (+ 1 2.3) -)"); - - evaluate_to::error_type>(R"( -(define x (do (b) (true 0))) -(eval x) )"); evaluate_to::error_type>(R"( @@ -589,71 +618,23 @@ TEST_CASE("get_list and get_list_range edge cases", "[implementation]") )") == true); } -TEST_CASE("scoped do expression", "[builtins]") +TEST_CASE("cond", "[builtins]") { - STATIC_CHECK(evaluate_to(R"( - -((lambda (count) - (do ((i 1 (+ i 1)) - (sum 0 (+ sum i))) - ((> i count) sum) - ) -) 10) - -)") == 55); - - // More complex examples - STATIC_CHECK(evaluate_to(R"( -(define sum-to - (lambda (n) - (do ((i 1 (+ i 1)) - (sum 0 (+ sum i))) - ((> i n) sum)))) -(sum-to 100) -)") == 5050); - - // Do with multiple statements in body - STATIC_CHECK(evaluate_to(R"( -(do ((i 1 (+ i 1)) - (sum 0 (+ sum i))) - ((> i 5) sum) - (define temp (* i 2)) - (* temp 1)) -)") == 15); + STATIC_CHECK(evaluate_to("(cond (else 42))") == 42); + STATIC_CHECK(evaluate_to("(cond (false 1) (else 42))") == 42); + STATIC_CHECK(evaluate_to("(cond (true 1) (else 42))") == 1); + STATIC_CHECK(evaluate_to("(cond (false 1) (true 2) (else 42))") == 2); + STATIC_CHECK(evaluate_to("(cond (true 1) (true 2) (else 42))") == 1); + STATIC_CHECK(evaluate_to("(cond (false 1) (false 2) (else 42))") == 42); + STATIC_CHECK(evaluate_to("(cond ((== 1 1) 1) (else 42))") == 1); } -TEST_CASE("iterative algorithmic tests", "[algorithms]") +TEST_CASE("begin", "[builtins]") { - // Test iterative algorithms that work in constexpr context - - // Compute sum of first 10 natural numbers - STATIC_CHECK(evaluate_to(R"( -(do ((i 1 (+ i 1)) - (sum 0 (+ sum i))) - ((> i 10) sum)) -)") == 55); - - // Count even numbers from 1 to 10 - // The test was failing because integer division behaves like C++ - // We need to check if i mod 2 equals 0 - STATIC_CHECK(evaluate_to(R"( -(do ((i 1 (+ i 1)) - (count 0 (if (== (- i (* (/ i 2) 2)) 0) (+ count 1) count))) - ((> i 10) count)) -)") == 5); - - // Square calculation - STATIC_CHECK(evaluate_to(R"( -(define square (lambda (x) (* x x))) -(square 6) -)") == 36); - - // Iterative GCD calculation instead of recursive - STATIC_CHECK(evaluate_to(R"( -(do ((a 48 b) - (b 18 (- a (* (/ a b) b)))) - ((== b 0) a)) -)") == 6); + STATIC_CHECK(evaluate_to("(begin true)") == true); + STATIC_CHECK(evaluate_to("(begin true false)") == false); + STATIC_CHECK(evaluate_to("(begin true false 1)") == 1); + STATIC_CHECK(evaluate_to("(begin true false (* 3 3))") == 9); } TEST_CASE("basic for-each usage", "[builtins]") @@ -694,13 +675,6 @@ TEST_CASE("token parsing edge cases", "[parsing]") TEST_CASE("Quoted symbol equality issues", "[symbols]") { - // These tests currently fail but should work based on the expected behavior of symbols - // They are included to document expected behavior and prevent regression - - // ---------------------------------------- - // FAILING CASES - Should all return true - // ---------------------------------------- - // 1. Direct quoted symbol equality fails STATIC_CHECK(evaluate_to("(== 'hello 'hello)") == true); @@ -743,7 +717,7 @@ TEST_CASE("Quoted symbol equality issues", "[symbols]") TEST_CASE("IndexedString creation and comparison", "[core][indexedstring]") { constexpr auto test_indexed_string_creation = []() { - lefticus::IndexedString str{ 5, 10 }; + lefticus::IndexedString const str{ 5, 10 }; return str.start == 5 && str.size == 10; }; STATIC_CHECK(test_indexed_string_creation()); @@ -752,8 +726,8 @@ TEST_CASE("IndexedString creation and comparison", "[core][indexedstring]") TEST_CASE("IndexedString equality", "[core][indexedstring]") { constexpr auto test_indexed_string_equality = []() { - lefticus::IndexedString str1{ 5, 10 }; - lefticus::IndexedString str2{ 5, 10 }; + lefticus::IndexedString const str1{ 5, 10 }; + lefticus::IndexedString const str2{ 5, 10 }; return str1 == str2; }; STATIC_CHECK(test_indexed_string_equality()); @@ -762,8 +736,8 @@ TEST_CASE("IndexedString equality", "[core][indexedstring]") TEST_CASE("IndexedString inequality", "[core][indexedstring]") { constexpr auto test_indexed_string_inequality = []() { - lefticus::IndexedString str1{ 5, 10 }; - lefticus::IndexedString str2{ 15, 10 }; + lefticus::IndexedString const str1{ 5, 10 }; + lefticus::IndexedString const str2{ 15, 10 }; return str1 != str2; }; STATIC_CHECK(test_indexed_string_inequality()); @@ -772,7 +746,7 @@ TEST_CASE("IndexedString inequality", "[core][indexedstring]") TEST_CASE("IndexedString substr", "[core][indexedstring]") { constexpr auto test_indexed_string_substr = []() { - lefticus::IndexedString str{ 5, 10 }; + lefticus::IndexedString const str{ 5, 10 }; auto substr = str.substr(2); return substr.start == 7 && substr.size == 8; }; @@ -783,7 +757,7 @@ TEST_CASE("IndexedString substr", "[core][indexedstring]") TEST_CASE("IndexedList creation and properties", "[core][indexedlist]") { constexpr auto test_indexed_list_creation = []() { - lefticus::IndexedList list{ 10, 5 }; + lefticus::IndexedList const list{ .start = 10, .size = 5 }; return list.start == 10 && list.size == 5 && !list.empty(); }; STATIC_CHECK(test_indexed_list_creation()); @@ -792,8 +766,8 @@ TEST_CASE("IndexedList creation and properties", "[core][indexedlist]") TEST_CASE("IndexedList equality", "[core][indexedlist]") { constexpr auto test_indexed_list_equality = []() { - lefticus::IndexedList list1{ 10, 5 }; - lefticus::IndexedList list2{ 10, 5 }; + lefticus::IndexedList const list1{ .start = 10, .size = 5 }; + lefticus::IndexedList const list2{ .start = 10, .size = 5 }; return list1 == list2; }; STATIC_CHECK(test_indexed_list_equality()); @@ -802,7 +776,7 @@ TEST_CASE("IndexedList equality", "[core][indexedlist]") TEST_CASE("IndexedList element access", "[core][indexedlist]") { constexpr auto test_indexed_list_access = []() { - lefticus::IndexedList list{ 10, 5 }; + lefticus::IndexedList const list{ .start = 10, .size = 5 }; return list.front() == 10 && list[2] == 12 && list.back() == 14; }; STATIC_CHECK(test_indexed_list_access()); @@ -811,7 +785,7 @@ TEST_CASE("IndexedList element access", "[core][indexedlist]") TEST_CASE("IndexedList sublist operations", "[core][indexedlist]") { constexpr auto test_indexed_list_sublist = []() { - lefticus::IndexedList list{ 10, 5 }; + lefticus::IndexedList const list{ .start = 10, .size = 5 }; auto sublist1 = list.sublist(2); auto sublist2 = list.sublist(1, 3); return (sublist1.start == 12 && sublist1.size == 3) && (sublist2.start == 11 && sublist2.size == 3); @@ -823,8 +797,8 @@ TEST_CASE("IndexedList sublist operations", "[core][indexedlist]") TEST_CASE("Identifier creation and properties", "[core][identifier]") { constexpr auto test_identifier_creation = []() { - lefticus::Identifier id{ lefticus::IndexedString{ 5, 10 } }; - return id.value.start == 5 && id.value.size == 10; + lefticus::Identifier const id{ 5, 10 }; + return id.start == 5 && id.size == 10; }; STATIC_CHECK(test_identifier_creation()); } @@ -832,8 +806,8 @@ TEST_CASE("Identifier creation and properties", "[core][identifier]") TEST_CASE("Identifier equality", "[core][identifier]") { constexpr auto test_identifier_equality = []() { - lefticus::Identifier id1{ lefticus::IndexedString{ 5, 10 } }; - lefticus::Identifier id2{ lefticus::IndexedString{ 5, 10 } }; + lefticus::Identifier const id1{ 5, 10 }; + lefticus::Identifier const id2{ 5, 10 }; return id1 == id2; }; STATIC_CHECK(test_identifier_equality()); @@ -842,8 +816,8 @@ TEST_CASE("Identifier equality", "[core][identifier]") TEST_CASE("Identifier inequality", "[core][identifier]") { constexpr auto test_identifier_inequality = []() { - constexpr lefticus::Identifier id1{ lefticus::IndexedString{ 5, 10 } }; - constexpr lefticus::Identifier id2{ lefticus::IndexedString{ 15, 10 } }; + constexpr lefticus::Identifier id1{ 5, 10 }; + constexpr lefticus::Identifier id2{ 15, 10 }; return id1 != id2; }; STATIC_CHECK(test_identifier_inequality()); @@ -852,9 +826,9 @@ TEST_CASE("Identifier inequality", "[core][identifier]") TEST_CASE("Identifier substr", "[core][identifier]") { constexpr auto test_identifier_substr = []() { - lefticus::Identifier id{ lefticus::IndexedString{ 5, 10 } }; + lefticus::Identifier const id{ 5, 10 }; auto substr = id.substr(2); - return substr.value.start == 7 && substr.value.size == 8; + return substr.start == 7 && substr.size == 8; }; STATIC_CHECK(test_identifier_substr()); } @@ -959,8 +933,8 @@ TEST_CASE("Parser interprets quoted symbols", "[core][parser][quotes]") constexpr auto token = parse_result.second; // it's actually expected that both "parsed" and "remaining" are empty here // because it consumed all input tokens and the last pass parsed nothing - STATIC_CHECK(token.parsed == ""); - STATIC_CHECK(token.remaining == ""); + STATIC_CHECK(token.parsed.empty()); + STATIC_CHECK(token.remaining.empty()); } @@ -1013,8 +987,23 @@ TEST_CASE("deeply nested expressions", "[nesting]") )") == true); } + TEST_CASE("quote function", "[builtins][quote]") { + STATIC_CHECK(evaluate_to("(+ (quote 1) (quote 2))") == 3); + STATIC_CHECK(evaluate_to("(+ (quote 1) '2)") == 3); + STATIC_CHECK(evaluate_to("(+ '1 '2)") == 3); + + STATIC_CHECK(evaluate_to("(== '1 '1)") == true); + STATIC_CHECK(evaluate_to("(== (quote 1) '1)") == true); + STATIC_CHECK(evaluate_to("(== (quote 1) (quote 1))") == true); + STATIC_CHECK(evaluate_to("(== '1 (quote 1))") == true); + + STATIC_CHECK(evaluate_to("(== ''1 (quote (quote 1)))") == true); + STATIC_CHECK(evaluate_to("(== ''a (quote (quote a)))") == true); + STATIC_CHECK(evaluate_to("(== ''ab (quote (quote ab)))") == true); + + // Basic quote tests with lists STATIC_CHECK(evaluate_to("(== (quote (1 2 3)) '(1 2 3))") == true); STATIC_CHECK(evaluate_to("(== (quote ()) '())") == true); @@ -1041,3 +1030,1124 @@ TEST_CASE("quote function", "[builtins][quote]") // Quote for expressions that would otherwise error STATIC_CHECK(evaluate_to("(== (quote (undefined-function 1 2)) '(undefined-function 1 2))") == true); } + +TEST_CASE("Type mismatch error handling", "[errors][types]") +{ + // Test mismatched type comparison errors + STATIC_CHECK(evaluate_to("(error? (< 1 \"string\"))") == true); + STATIC_CHECK(evaluate_to("(error? (> 1.0 '(1 2 3)))") == true); + STATIC_CHECK(evaluate_to("(error? (== \"hello\" 123))") == true); + STATIC_CHECK(evaluate_to("(error? (!= true 42))") == true); + + // Test arithmetic with mismatched types + STATIC_CHECK(evaluate_to("(error? (+ 1 \"2\"))") == true); + STATIC_CHECK(evaluate_to("(error? (* 3.14 \"pi\"))") == true); + + // Test errors from applying functions to wrong types + STATIC_CHECK(evaluate_to("(error? (car 42))") == true); + STATIC_CHECK(evaluate_to("(error? (cdr \"not a list\"))") == true); +} + +TEST_CASE("Error handling in diverse contexts", "[errors][edge]") +{ + // Test error from get_list with wrong size + STATIC_CHECK(evaluate_to("(error? (let ((x 1)) (apply + (x))))") == true); + + // Test divide by zero error + // STATIC_CHECK(evaluate_to("(error? (/ 1 0))") == true); + + // Test undefined variable access + STATIC_CHECK(evaluate_to("(error? undefined-var)") == true); + + // Test invalid function call + STATIC_CHECK(evaluate_to("(error? (1 2 3))") == true); + + // Test error in cond expression + STATIC_CHECK(evaluate_to("(error? (cond ((+ 1 \"x\") 10) (else 20)))") == true); + + // Test error in if condition + STATIC_CHECK(evaluate_to("(error? (if (< \"a\" 1) 10 20))") == true); +} + +TEST_CASE("Edge case behavior", "[edge][misc]") +{ + // Test nested expression evaluation with type errors + STATIC_CHECK(evaluate_to("(error? (+ 1 (+ 2 \"3\")))") == true); + + // Test lambda with mismatched argument counts + STATIC_CHECK(evaluate_to("(error? ((lambda (x y) (+ x y)) 1))") == true); + + // Test let with malformed bindings + STATIC_CHECK(evaluate_to("(error? (let (x 1) x))") == true); + STATIC_CHECK(evaluate_to("(error? (let ((x)) x))") == true); + + // Test define with non-identifier as first param + STATIC_CHECK(evaluate_to("(error? (define 123 456))") == true); + + // Test cons with too many arguments + STATIC_CHECK(evaluate_to("(error? (cons 1 2 3))") == true); + + // Test cond with non-boolean condition, this is an error, 123 does not evaluate to a bool + STATIC_CHECK(evaluate_to("(error? (cond (123 456) (else 789)))") == true); +} + +TEST_CASE("for-each function without side effects", "[builtins][for-each]") +{ + // Test for-each using immutable approach + STATIC_CHECK(evaluate_to(R"( + (let ((counter (lambda (count) + (lambda (x) (+ count 1))))) + (let ((result (for-each (counter 0) '(1 2 3 4 5)))) + 5)) + )") == 5); + + // Test for-each with empty list + STATIC_CHECK(evaluate_to(R"( + (for-each (lambda (x) x) '()) + )") == std::monostate{}); + + // Test for-each with non-list argument (should error) + STATIC_CHECK(evaluate_to("(error? (for-each (lambda (x) x) 42))") == true); +} + +// Branch Coverage Enhancement Tests - SmallVector Overflow + +TEST_CASE("SmallVector overflow scenarios for coverage", "[utility][coverage]") +{ + constexpr auto test_values_overflow = []() constexpr { + // Create engine with smaller capacity for testing + lefticus::cons_expr engine; + + // Test error state after exceeding capacity + for (int i = 0; i < 35; ++i) {// Exceed capacity + engine.values.insert(engine.True); + } + return engine.values.error_state; + }; + + STATIC_CHECK(test_values_overflow()); + + constexpr auto test_strings_overflow = []() constexpr { + lefticus::cons_expr engine; + + // Test string capacity overflow by adding many unique strings + for (int i = 0; i < 20; ++i) { + // Create unique strings to avoid deduplication + std::array buffer{}; + for (std::size_t j = 0; j < 25; ++j) { + buffer.at(j) = static_cast('a' + ((static_cast(i) + j) % 26)); + } + std::string_view const test_str{ buffer.data(), 25 }; + engine.strings.insert(test_str); + if (engine.strings.error_state) { + return true;// Successfully detected overflow + } + } + return false;// Should have overflowed by now + }; + + STATIC_CHECK(test_strings_overflow()); +} + +TEST_CASE("Scratch class move semantics and error paths", "[utility][coverage]") +{ + constexpr auto test_scratch_move = []() constexpr { + lefticus::cons_expr<> engine; + + // Test Scratch move constructor + auto create_scratch = [&]() { return lefticus::cons_expr<>::Scratch{ engine.object_scratch }; }; + + auto moved_scratch = create_scratch(); + moved_scratch.push_back(engine.True); + + return moved_scratch.end() - moved_scratch.begin() == 1; + }; + STATIC_CHECK(test_scratch_move()); + + // Test Scratch destructor behavior + constexpr auto test_scratch_destructor = []() constexpr { + lefticus::cons_expr<> engine; + auto initial_size = engine.object_scratch.size(); + + { + auto scratch = lefticus::cons_expr<>::Scratch{ engine.object_scratch }; + scratch.push_back(engine.True); + scratch.push_back(engine.False); + }// Destructor should reset size + + return engine.object_scratch.size() == initial_size; + }; + STATIC_CHECK(test_scratch_destructor()); +} + +TEST_CASE("Closure self-reference and recursion edge cases", "[evaluation][coverage]") +{ + constexpr auto test_closure_self_ref = []() constexpr { + lefticus::cons_expr<> engine; + + // Test closure without self-reference + auto [parsed, _] = engine.parse("(lambda (x) x)"); + auto closure_expr = engine.values[parsed[0]]; + auto result = engine.eval(engine.global_scope, closure_expr); + + if (const auto *closure = engine.get_if::Closure>(&result)) { + return !closure->has_self_reference(); + } + return false; + }; + STATIC_CHECK(test_closure_self_ref()); + + // Test complex recursive closure error case + constexpr auto test_recursive_closure_error = []() constexpr { + lefticus::cons_expr<> engine; + + // Test lambda with wrong parameter count + auto [parsed, _] = engine.parse("((lambda (x y) (+ x y)) 5)");// Missing second parameter + auto result = engine.eval(engine.global_scope, engine.values[parsed[0]]); + + return std::holds_alternative::error_type>(result.value); + }; + STATIC_CHECK(test_recursive_closure_error()); +} + +TEST_CASE("List bounds checking and error conditions", "[evaluation][coverage]") +{ + constexpr auto test_get_list_bounds = []() constexpr { + lefticus::cons_expr<> engine; + + // Test get_list with size bounds + auto [parsed, _] = engine.parse("(1 2 3)"); + auto list_expr = engine.values[parsed[0]]; + + // Test minimum bound violation + auto result1 = engine.get_list(list_expr, "test", 5, 10); + if (result1.has_value()) { return false; } + + // Test maximum bound violation + auto result2 = engine.get_list(list_expr, "test", 0, 2); + if (result2.has_value()) { return false; } + + // Test non-list type + auto result3 = engine.get_list(engine.True, "test"); + return !result3.has_value(); + }; + STATIC_CHECK(test_get_list_bounds()); + + // Test get_list_range error propagation + constexpr auto test_get_list_range_errors = []() constexpr { + lefticus::cons_expr<> engine; + + auto result = engine.get_list_range(engine.True, "expected list", 1, 5); + return !result.has_value(); + }; + STATIC_CHECK(test_get_list_range_errors()); +} + +TEST_CASE("Complex parsing edge cases and malformed expressions", "[parser][coverage]") +{ + // Test malformed let expressions + constexpr auto test_malformed_let = []() constexpr { + lefticus::cons_expr<> engine; + + // Test let with malformed variable list + auto result1 = engine.evaluate("(let (x) x)");// Missing value for x + if (!std::holds_alternative::error_type>(result1.value)) { return false; } + + // Test let with non-identifier variable name + auto result2 = engine.evaluate("(let ((42 100)) 42)");// Number as variable name + if (!std::holds_alternative::error_type>(result2.value)) { return false; } + + return true; + }; + STATIC_CHECK(test_malformed_let()); + + // Test malformed define expressions + constexpr auto test_malformed_define = []() constexpr { + lefticus::cons_expr<> engine; + + // Test define with non-identifier name + auto [parsed, _] = engine.parse("(define 42 100)"); + auto result = engine.eval(engine.global_scope, engine.values[parsed[0]]); + + return std::holds_alternative::error_type>(result.value); + }; + STATIC_CHECK(test_malformed_define()); + + // Test parsing edge cases with quotes and parentheses + constexpr auto test_parsing_edge_cases = []() constexpr { + lefticus::cons_expr<> engine; + + // Test unterminated quote depth tracking + auto [parsed1, remaining1] = engine.parse("'(1 2"); + // Should have parsed the quote but left unclosed parenthesis + (void)parsed1; + (void)remaining1;// Suppress unused warnings + + // Test empty parentheses + auto result2 = engine.evaluate("()"); + if (std::holds_alternative::error_type>(result2.value)) { return false; } + + // Test multiple quote levels + auto result3 = engine.evaluate("'''symbol"); + return !std::holds_alternative::error_type>(result3.value); + }; + STATIC_CHECK(test_parsing_edge_cases()); +} + +TEST_CASE("Function invocation error paths and type mismatches", "[evaluation][coverage]") +{ + // Test function invocation with non-function + constexpr auto test_invalid_function = []() constexpr { + lefticus::cons_expr<> engine; + + auto result = engine.evaluate("(42 1 2 3)");// Try to call number as function + return std::holds_alternative::error_type>(result.value); + }; + STATIC_CHECK(test_invalid_function()); + + // Test parameter type mismatch in built-in functions + constexpr auto test_type_mismatch = []() constexpr { + lefticus::cons_expr<> engine; + + // Test arithmetic with wrong types + auto result1 = engine.evaluate("(+ 1 \"hello\")"); + if (!std::holds_alternative::error_type>(result1.value)) { return false; } + + // Test car with non-list + auto result2 = engine.evaluate("(car 42)"); + if (!std::holds_alternative::error_type>(result2.value)) { return false; } + + // Test cdr with non-list + auto result3 = engine.evaluate("(cdr \"hello\")"); + return std::holds_alternative::error_type>(result3.value); + }; + STATIC_CHECK(test_type_mismatch()); + + // Test eval_to template with wrong parameter count + constexpr auto test_eval_to_errors = []() constexpr { + lefticus::cons_expr<> engine; + + // Test cons with wrong parameter count + auto result1 = engine.evaluate("(cons 1)");// Need 2 parameters + if (!std::holds_alternative::error_type>(result1.value)) { return false; } + + // Test append with wrong parameter count + auto result2 = engine.evaluate("(append '(1 2))");// Need 2 lists + return std::holds_alternative::error_type>(result2.value); + }; + STATIC_CHECK(test_eval_to_errors()); +} + +TEST_CASE("Advanced error handling and edge cases", "[evaluation][coverage]") +{ + // Test cond with complex conditions and error handling + constexpr auto test_cond_errors = []() constexpr { + lefticus::cons_expr<> engine; + + // Test cond with non-boolean condition that errors + auto result1 = engine.evaluate("(cond ((car 42) 1) (else 2))"); + if (!std::holds_alternative::error_type>(result1.value)) { return false; } + + // Test cond with malformed clauses + auto result2 = engine.evaluate("(cond (true))");// Missing action + return std::holds_alternative::error_type>(result2.value); + }; + STATIC_CHECK(test_cond_errors()); + + // Test complex nested error propagation + constexpr auto test_nested_errors = []() constexpr { + lefticus::cons_expr<> engine; + + // Test error in nested function call + auto result = engine.evaluate("(+ 1 (car (cdr '(1))))");// cdr of single element list + return std::holds_alternative::error_type>(result.value); + }; + STATIC_CHECK(test_nested_errors()); + + // Test string processing with buffer overflow edge case + constexpr auto test_string_buffer_edge = []() constexpr { + lefticus::cons_expr engine;// Small buffer + + // Create a very long string with many escape sequences + std::string long_str = "\""; + for (int i = 0; i < 100; ++i) { long_str += "\\n\\t"; } + long_str += "\""; + + auto result = engine.evaluate(long_str); + (void)result;// Suppress unused warning + // Should either succeed or fail gracefully + return true;// Any outcome is acceptable for this edge case + }; + STATIC_CHECK(test_string_buffer_edge()); +} + +TEST_CASE("Number parsing edge cases and arithmetic operations", "[parser][arithmetic][coverage]") +{ + // Test number parsing edge cases + constexpr auto test_number_parsing_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test floating point operations with special values + auto result1 = engine.evaluate("(+ 1.5 2.7)"); + if (std::holds_alternative::error_type>(result1.value)) { return false; } + + // Test negative number operations + auto result2 = engine.evaluate("(* -1 42)"); + const auto *int_ptr = engine.get_if(&result2); + if (!int_ptr || *int_ptr != -42) { return false; } + + // Test multiple arithmetic operations + auto result3 = engine.evaluate("(+ (* 2 3) (- 10 4))"); + const auto *int_ptr3 = engine.get_if(&result3); + return int_ptr3 && *int_ptr3 == 12; + }; + STATIC_CHECK(test_number_parsing_edges()); + + // Test comparison operations with mixed types + constexpr auto test_comparison_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test string comparisons + auto result1 = engine.evaluate(R"((== "hello" "hello"))"); + const auto *bool_ptr1 = engine.get_if(&result1); + if (!bool_ptr1 || !*bool_ptr1) { return false; } + + // Test list comparisons + auto result2 = engine.evaluate("(== '(1 2) '(1 2))"); + const auto *bool_ptr2 = engine.get_if(&result2); + return bool_ptr2 && *bool_ptr2; + }; + STATIC_CHECK(test_comparison_edges()); + + // Test mathematical operations with edge values + constexpr auto test_math_edge_values = []() constexpr { + lefticus::cons_expr<> engine; + + // Test subtraction resulting in negative + auto result1 = engine.evaluate("(- 3 5)"); + const auto *int_ptr1 = engine.get_if(&result1); + if (!int_ptr1 || *int_ptr1 != -2) { return false; } + + // Test multiplication by zero + auto result2 = engine.evaluate("(* 42 0)"); + const auto *int_ptr2 = engine.get_if(&result2); + return int_ptr2 && *int_ptr2 == 0; + }; + STATIC_CHECK(test_math_edge_values()); +} + +TEST_CASE("Conditional expression and control flow coverage", "[evaluation][control][coverage]") +{ + // Test cond with various condition types + constexpr auto test_cond_variations = []() constexpr { + lefticus::cons_expr<> engine; + + // Test cond with else clause + auto result1 = engine.evaluate("(cond (false 1) (else 2))"); + const auto *int_ptr1 = engine.get_if(&result1); + if (!int_ptr1 || *int_ptr1 != 2) { return false; } + + // Test cond with multiple false conditions + auto result2 = engine.evaluate("(cond (false 1) (false 2) (true 3))"); + const auto *int_ptr2 = engine.get_if(&result2); + return int_ptr2 && *int_ptr2 == 3; + }; + STATIC_CHECK(test_cond_variations()); + + // Test if statement edge cases + constexpr auto test_if_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test if with complex condition + auto result1 = engine.evaluate("(if (== 1 1) (+ 2 3) (* 2 3))"); + const auto *int_ptr1 = engine.get_if(&result1); + if (!int_ptr1 || *int_ptr1 != 5) { return false; } + + // Test if with false condition + auto result2 = engine.evaluate("(if (== 1 2) 10 20)"); + const auto *int_ptr2 = engine.get_if(&result2); + return int_ptr2 && *int_ptr2 == 20; + }; + STATIC_CHECK(test_if_edges()); + + // Test logical operations short-circuiting + constexpr auto test_logical_short_circuit = []() constexpr { + lefticus::cons_expr<> engine; + + // Test 'and' short-circuiting (should not evaluate second part if first is false) + auto result1 = engine.evaluate("(and false (car 42))");// Second part would error if evaluated + const auto *bool_ptr1 = engine.get_if(&result1); + if (!bool_ptr1 || *bool_ptr1 != false) { return false; } + + // Test 'or' short-circuiting (should not evaluate second part if first is true) + auto result2 = engine.evaluate("(or true (car 42))");// Second part would error if evaluated + const auto *bool_ptr2 = engine.get_if(&result2); + return bool_ptr2 && *bool_ptr2 == true; + }; + STATIC_CHECK(test_logical_short_circuit()); +} + +TEST_CASE("Template specialization and type handling coverage", "[types][templates][coverage]") +{ + // Test get_if with different types + constexpr auto test_get_if_variants = []() constexpr { + lefticus::cons_expr<> engine; + + auto [parsed, _] = engine.parse("42"); + auto expr = engine.values[parsed[0]]; + + // Test get_if with correct type + const auto *int_ptr = engine.get_if(&expr); + if (int_ptr == nullptr || *int_ptr != 42) { return false; } + + // Test get_if with wrong type (should return nullptr) + const auto *str_ptr = engine.get_if::string_type>(&expr); + return str_ptr == nullptr; + }; + STATIC_CHECK(test_get_if_variants()); + + // Test type predicates with various types + constexpr auto test_type_predicates = []() constexpr { + lefticus::cons_expr<> engine; + + // Test integer? predicate + auto result1 = engine.evaluate("(integer? 42)"); + const auto *bool_ptr1 = engine.get_if(&result1); + if (!bool_ptr1 || !*bool_ptr1) { return false; } + + // Test string? predicate + auto result2 = engine.evaluate("(string? \"hello\")"); + const auto *bool_ptr2 = engine.get_if(&result2); + if (!bool_ptr2 || !*bool_ptr2) { return false; } + + // Test boolean? predicate + auto result3 = engine.evaluate("(boolean? true)"); + const auto *bool_ptr3 = engine.get_if(&result3); + return bool_ptr3 && *bool_ptr3; + }; + STATIC_CHECK(test_type_predicates()); + + // Test eval_to template with different parameter counts + constexpr auto test_eval_to_templates = []() constexpr { + lefticus::cons_expr<> engine; + + // Test single parameter eval_to with constructed SExpr + lefticus::cons_expr<>::SExpr const test_expr{ lefticus::cons_expr<>::Atom{ 42 } }; + auto result1 = engine.eval_to(engine.global_scope, test_expr); + if (!result1.has_value() || result1.value() != 42) { return false; } + + // Test template with wrong type - should fail type conversion + auto result2 = engine.eval_to(engine.global_scope, test_expr); + return !result2.has_value();// Should fail type conversion + }; + STATIC_CHECK(test_eval_to_templates()); +} + +TEST_CASE("Advanced list operations and memory management", "[lists][memory][coverage]") +{ + // Test cons with different value combinations + constexpr auto test_cons_variations = []() constexpr { + lefticus::cons_expr<> engine; + + // Test cons with atom and list + auto result1 = engine.evaluate("(cons 1 '(2 3))"); + const auto *list1 = engine.get_if::literal_list_type>(&result1); + if (list1 == nullptr) { return false; } + + // Test cons with list and list + auto result2 = engine.evaluate("(cons '(a) '(b c))"); + const auto *list2 = engine.get_if::literal_list_type>(&result2); + return list2 != nullptr; + }; + STATIC_CHECK(test_cons_variations()); + + // Test append with edge cases + constexpr auto test_append_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test appending empty lists + auto result1 = engine.evaluate("(append '() '(1 2))"); + const auto *list1 = engine.get_if::literal_list_type>(&result1); + if (list1 == nullptr) { return false; } + + // Test appending to empty list + auto result2 = engine.evaluate("(append '(1 2) '())"); + const auto *list2 = engine.get_if::literal_list_type>(&result2); + return list2 != nullptr; + }; + STATIC_CHECK(test_append_edges()); + + // Test car/cdr with various list types + constexpr auto test_car_cdr_variants = []() constexpr { + lefticus::cons_expr<> engine; + + // Test car with single element list + auto result1 = engine.evaluate("(car '(42))"); + const auto *int_ptr1 = engine.get_if(&result1); + if (!int_ptr1 || *int_ptr1 != 42) { return false; } + + // Test cdr with two element list + auto result2 = engine.evaluate("(cdr '(1 2))"); + const auto *list2 = engine.get_if::literal_list_type>(&result2); + return list2 != nullptr; + }; + STATIC_CHECK(test_car_cdr_variants()); +} + +TEST_CASE("Parser token handling and quote processing", "[parser][tokens][coverage]") +{ + // Test different quote levels and combinations + constexpr auto test_quote_combinations = []() constexpr { + lefticus::cons_expr<> engine; + + // Test nested quotes + auto result1 = engine.evaluate("''symbol"); + static_cast(result1);// Suppress unused variable warning + // Should create a nested quote structure + + // Test quote with lists + auto result2 = engine.evaluate("'(+ 1 2)"); + const auto *list2 = engine.get_if::literal_list_type>(&result2); + if (list2 == nullptr) { return false; } + + // Test quote with mixed content + auto result3 = engine.evaluate("'(a 1 \"hello\")"); + const auto *list3 = engine.get_if::literal_list_type>(&result3); + return list3 != nullptr; + }; + STATIC_CHECK(test_quote_combinations()); + + // Test token parsing with various delimiters + constexpr auto test_token_delimiters = []() constexpr { + lefticus::cons_expr<> engine; + + // Test parsing with tabs and multiple spaces + auto [parsed1, remaining1] = engine.parse(" \t 42 \t "); + if (parsed1.size != 1) { return false; } + + // Test parsing with mixed whitespace + auto [parsed2, remaining2] = engine.parse("\n\r(+ 1 2)\n"); + return parsed2.size == 1; + }; + STATIC_CHECK(test_token_delimiters()); + + // Test string parsing with various escape sequences + constexpr auto test_string_escapes = []() constexpr { + lefticus::cons_expr<> engine; + + // Test all supported escape sequences + auto result1 = engine.evaluate(R"("\n\t\r\f\b\"\\")"); + const auto *str1 = engine.get_if::string_type>(&result1); + if (str1 == nullptr) { return false; } + + // Test string with mixed content + auto result2 = engine.evaluate(R"("Hello\nWorld")"); + const auto *str2 = engine.get_if::string_type>(&result2); + return str2 != nullptr; + }; + STATIC_CHECK(test_string_escapes()); +} + +TEST_CASE("SmallVector overflow and division operations", "[coverage][memory][math]") +{ + // Test step by step to isolate the issue + constexpr auto test_step1 = []() constexpr { + lefticus::cons_expr<> engine; + auto result1 = engine.evaluate("(cons 1 '())"); + // Try both list_type and literal_list_type to see which one works + const auto *list1 = engine.get_if::list_type>(&result1); + const auto *literal_list1 = engine.get_if::literal_list_type>(&result1); + return list1 != nullptr || literal_list1 != nullptr; + }; + STATIC_CHECK(test_step1()); + + constexpr auto test_step2 = []() constexpr { + lefticus::cons_expr<> engine; + auto result2 = engine.evaluate("(+ 10 2)"); + const auto *int_ptr2 = engine.get_if(&result2); + return int_ptr2 != nullptr && *int_ptr2 == 12; + }; + STATIC_CHECK(test_step2()); + + constexpr auto test_step3 = []() constexpr { + lefticus::cons_expr<> engine; + auto result3 = engine.evaluate("(* 3 4)"); + const auto *int_ptr3 = engine.get_if(&result3); + return int_ptr3 != nullptr && *int_ptr3 == 12; + }; + STATIC_CHECK(test_step3()); +} + +TEST_CASE("Error path and edge case coverage targeting specific uncovered branches", "[error][coverage][edge]") +{ + // Test number parsing failures (line 263 - parse_number failure cases) + constexpr auto test_number_parse_failures = []() constexpr { + lefticus::cons_expr<> engine; + + // Test parsing just a minus sign (should fail) + auto [parsed1, remaining1] = engine.parse("-"); + if (parsed1.size != 1) { return false; } + auto result1 = engine.values[parsed1[0]]; + // Should be parsed as identifier, not number + const auto *id1 = engine.get_if::identifier_type>(&result1); + if (id1 == nullptr) { return false; } + + // Test malformed numbers + auto [parsed2, remaining2] = engine.parse("1.2.3"); + if (parsed2.size != 1) { return false; } + auto result2 = engine.values[parsed2[0]]; + // Should be parsed as identifier since it's not a valid number + const auto *id2 = engine.get_if::identifier_type>(&result2); + return id2 != nullptr; + }; + STATIC_CHECK(test_number_parse_failures()); + + // Test function invocation errors (line 860-868 - invoke_function error paths) + constexpr auto test_function_invoke_errors = []() constexpr { + lefticus::cons_expr<> engine; + + // Test calling non-function as function + auto result1 = engine.evaluate("(42 1 2)");// Try to call number as function + if (!std::holds_alternative::error_type>(result1.value)) { return false; } + + // Test calling undefined function + auto result2 = engine.evaluate("(undefined-func 1 2)"); + // This should either be an error or return the undefined identifier + const auto *error2 = engine.get_if::error_type>(&result2); + const auto *id2 = engine.get_if::identifier_type>(&result2); + return error2 != nullptr || id2 != nullptr; + }; + STATIC_CHECK(test_function_invoke_errors()); + + // Test list access errors (car/cdr on empty or invalid lists) + constexpr auto test_list_access_errors = []() constexpr { + lefticus::cons_expr<> engine; + + // Test car on empty list - actually returns error based on CLI test + auto result1 = engine.evaluate("(car '())"); + // Should return error + const auto *error1 = engine.get_if::error_type>(&result1); + if (error1 == nullptr) { return false; } + + // Test cdr on empty list - now returns error (consistent with car) + auto result2 = engine.evaluate("(cdr '())"); + const auto *error2 = engine.get_if::error_type>(&result2); + if (error2 == nullptr) { return false; } + + // Test car on non-list - should return error + auto result3 = engine.evaluate("(car 42)"); + const auto *error3 = engine.get_if::error_type>(&result3); + return error3 != nullptr; + }; + STATIC_CHECK(test_list_access_errors()); +} + +TEST_CASE("Parser edge cases and malformed input coverage", "[parser][error][coverage]") +{ + // Test string parsing edge cases + constexpr auto test_string_parse_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test unterminated string (should be handled gracefully) + auto [parsed1, _] = engine.parse("\"unterminated"); + // Parser should handle this - either as error or incomplete parse + if (parsed1.size > 1) { + return false;// Should not create multiple tokens + } + + // Test string with invalid escape sequence + auto result1 = engine.evaluate(R"("\x")");// Invalid escape + // Should either parse successfully (treating as literal) or error + const auto *str1 = engine.get_if::string_type>(&result1); + const auto *error1 = engine.get_if::error_type>(&result1); + return str1 != nullptr || error1 != nullptr; + }; + STATIC_CHECK(test_string_parse_edges()); + + // Test expression parsing with malformed input + constexpr auto test_malformed_expressions = []() constexpr { + lefticus::cons_expr<> engine; + + // Test unmatched parentheses + auto [parsed1, _] = engine.parse("(+ 1 2");// Missing closing paren + static_cast(parsed1);// Suppress unused variable warning + // Should handle gracefully - either empty parse or partial + + // Test empty expression in parentheses + auto result1 = engine.evaluate("()"); + // Check what type it actually returns - could be literal_list_type or list_type + const auto *literal_list1 = engine.get_if::literal_list_type>(&result1); + const auto *list1 = engine.get_if::list_type>(&result1); + return literal_list1 != nullptr || list1 != nullptr; + }; + STATIC_CHECK(test_malformed_expressions()); + + // Test comment and whitespace edge cases + constexpr auto test_comment_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test comment at end of line without newline + auto [parsed1, remaining1] = engine.parse("42 ; comment"); + if (parsed1.size != 1) { return false; } + + // Test multiple consecutive comments + auto [parsed2, remaining2] = engine.parse("; comment1\n; comment2\n42"); + // Comments might affect parsing, just check it doesn't crash + static_cast(parsed2);// Suppress unused variable warning + return true;// Just ensure it doesn't crash + }; + STATIC_CHECK(test_comment_edges()); +} + +TEST_CASE("Type conversion and mathematical edge cases", "[math][types][coverage]") +{ + // Test mathematical operations with type mismatches + constexpr auto test_math_type_errors = []() constexpr { + lefticus::cons_expr<> engine; + + // Test addition with non-numeric types + auto result1 = engine.evaluate("(+ \"hello\" 42)"); + // Should result in error + if (!std::holds_alternative::error_type>(result1.value)) { return false; } + + // Test multiplication with mixed invalid types + auto result2 = engine.evaluate("(* true false)"); + return std::holds_alternative::error_type>(result2.value); + }; + STATIC_CHECK(test_math_type_errors()); + + // Test comparison operations with different types + constexpr auto test_comparison_type_mismatches = []() constexpr { + lefticus::cons_expr<> engine; + + // Test comparing incompatible types - returns error based on CLI test + auto result1 = engine.evaluate("(< \"hello\" 42)"); + // Returns error for incompatible types + const auto *error1 = engine.get_if::error_type>(&result1); + if (error1 == nullptr) { return false; } + + // Test equality with different types - also returns error + auto result2 = engine.evaluate("(== 42 \"42\")"); + const auto *error2 = engine.get_if::error_type>(&result2); + return error2 != nullptr;// Should return error for type mismatch + }; + STATIC_CHECK(test_comparison_type_mismatches()); + + // Test floating point edge cases + constexpr auto test_float_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test very small floating point numbers + auto result1 = engine.evaluate("(+ 0.000001 0.000002)"); + const auto *float1 = engine.get_if(&result1); + if (float1 == nullptr) { return false; } + + // Test floating point comparison precision + auto result2 = engine.evaluate("(== 0.1 0.1)"); + const auto *bool2 = engine.get_if(&result2); + return bool2 != nullptr && *bool2; + }; + STATIC_CHECK(test_float_edges()); +} + +TEST_CASE("Advanced control flow and scope edge cases", "[control][scope][coverage]") +{ + // Test nested scope edge cases + constexpr auto test_nested_scopes = []() constexpr { + lefticus::cons_expr<> engine; + + // Test deeply nested let expressions + auto result1 = engine.evaluate("(let ((x 1)) (let ((y 2)) (let ((z 3)) (+ x y z))))"); + const auto *int1 = engine.get_if(&result1); + if (!int1 || *int1 != 6) { return false; } + + // Test variable shadowing in nested scopes + auto result2 = engine.evaluate("(let ((x 1)) (let ((x 2)) x))"); + const auto *int2 = engine.get_if(&result2); + return int2 != nullptr && *int2 == 2; + }; + STATIC_CHECK(test_nested_scopes()); + + // Test lambda edge cases + constexpr auto test_lambda_edges = []() constexpr { + lefticus::cons_expr<> engine; + + // Test lambda with no parameters + auto result1 = engine.evaluate("((lambda () 42))"); + const auto *int1 = engine.get_if(&result1); + if (!int1 || *int1 != 42) { return false; } + + // Test lambda with wrong number of arguments + auto result2 = engine.evaluate("((lambda (x) (+ x 1)) 1 2)");// Too many args + // Should either work (ignoring extra) or error + const auto *int2 = engine.get_if(&result2); + const auto *error2 = engine.get_if::error_type>(&result2); + return int2 != nullptr || error2 != nullptr; + }; + STATIC_CHECK(test_lambda_edges()); + + // Test cond edge cases with complex conditions + constexpr auto test_cond_complex = []() constexpr { + lefticus::cons_expr<> engine; + + // Test cond with no matching conditions and no else + auto result1 = engine.evaluate("(cond (false 1) (false 2))"); + // Should return some default value or error + const auto *error1 = engine.get_if::error_type>(&result1); + if (error1 == nullptr) { + // Might return unspecified value, just check it doesn't crash + } + + // Test cond with complex nested conditions + auto result2 = engine.evaluate("(cond ((< 1 2) (+ 3 4)) (else 0))"); + const auto *int2 = engine.get_if(&result2); + return int2 != nullptr && *int2 == 7; + }; + STATIC_CHECK(test_cond_complex()); +} + +TEST_CASE("Branch coverage improvement - SmallVector and error paths", "[coverage][utility]") +{ + // Test SmallVector error state when exceeding capacity (lines 187, 192, 196) + constexpr auto test_smallvector_overflow = []() constexpr { + lefticus::SmallVector vec;// Small capacity + vec.insert(1); + vec.insert(2); + vec.insert(3); + auto idx = vec.insert(4);// This should set error_state + return vec.error_state && idx == 3;// Should return last valid index + }; + STATIC_CHECK(test_smallvector_overflow()); + + // Test resize with size > capacity (line 187) + constexpr auto test_resize_overflow = []() constexpr { + lefticus::SmallVector vec; + vec.resize(10);// Exceeds capacity + return vec.error_state && vec.size() == 5;// Size capped at capacity + }; + STATIC_CHECK(test_resize_overflow()); +} + +TEST_CASE("Branch coverage - Number parsing edge cases", "[coverage][parser]") +{ + STATIC_CHECK(lefticus::parse_number("-").first == false); + STATIC_CHECK(lefticus::parse_number("1.5e10").first == true); + STATIC_CHECK(lefticus::parse_number("1.5E10").first == true); + STATIC_CHECK(lefticus::parse_number("1.5e-2").second == 0.015); + STATIC_CHECK(lefticus::parse_number("1.5ex").first == false); + STATIC_CHECK(lefticus::parse_number("1.5e").first == false); + STATIC_CHECK(lefticus::parse_number(".5").second == .5); +} + +TEST_CASE("Branch coverage - Token parsing edge cases", "[coverage][parser]") +{ + STATIC_CHECK(lefticus::next_token("\r\ntest").parsed == "test"); + STATIC_CHECK(lefticus::next_token("\r test").parsed == "test"); + + STATIC_CHECK([]() { + auto [parsed, remaining] = lefticus::next_token("'symbol"); + return parsed == "'" && remaining == "symbol"; + }()); + + STATIC_CHECK([]() { + auto [parsed, remaining] = lefticus::next_token("(test)"); + return parsed == "(" && remaining == "test)"; + }()); + + STATIC_CHECK([]() { + auto [parsed, remaining] = lefticus::next_token(")rest"); + return parsed == ")" && remaining == "rest"; + }()); + + STATIC_CHECK([]() { + auto [parsed, remaining] = lefticus::next_token("\"unterminated"); + return parsed == "\"unterminated" && remaining.empty(); + }()); + + STATIC_CHECK([]() { + auto [parsed, remaining] = lefticus::next_token(""); + return parsed.empty() && remaining.empty(); + }()); +} + +TEST_CASE("Branch coverage - String escape sequences", "[coverage][strings]") +{ + // Test process_string_escapes edge cases (lines 538, 548) + constexpr auto test_escapes = []() constexpr { + lefticus::cons_expr<> engine; + + // Test unterminated escape (line 548) + auto result1 = engine.process_string_escapes("test\\"); + const auto *error1 = engine.get_if::error_type>(&result1); + if (error1 == nullptr) { return false; } + + // Test unknown escape char (line 538) + auto result2 = engine.process_string_escapes("test\\q"); + const auto *error2 = engine.get_if::error_type>(&result2); + if (error2 == nullptr) { return false; } + + // Test valid escapes + auto result3 = engine.process_string_escapes(R"(\n\t\r\"\\)"); + const auto *string3 = engine.get_if::string_type>(&result3); + return string3 != nullptr; + }; + STATIC_CHECK(test_escapes()); +} + +TEST_CASE("Branch coverage - Error type operations", "[coverage][error]") +{ + // Test Error equality operator (line 494) + constexpr auto test_error_ops = []() constexpr { + using Error = lefticus::Error; + lefticus::IndexedString const msg1{ 0, 10 }; + lefticus::IndexedString const msg2{ 0, 10 }; + lefticus::IndexedList const list1{ .start = 0, .size = 5 }; + lefticus::IndexedList const list2{ .start = 0, .size = 5 }; + + const Error error1{ msg1, list1 }; + const Error error2{ msg2, list2 }; + const Error error3{ msg1, lefticus::IndexedList{ .start = 1, .size = 5 } }; + + return error1 == error2 && !(error1 == error3); + }; + STATIC_CHECK(test_error_ops()); +} + +TEST_CASE("Branch coverage - Fix identifiers edge cases", "[coverage][parser]") +{ + // Test fix_identifiers branches (lines 1175-1180, 1191, 1196) + constexpr auto test_fix_identifiers = []() constexpr { + lefticus::cons_expr<> engine; + + // Test lambda identifier fixing + auto result1 = engine.evaluate("(lambda (x) (+ x 1))"); + const auto *closure1 = engine.get_if::Closure>(&result1); + if (closure1 == nullptr) { return false; } + + // Test let identifier fixing + auto result2 = engine.evaluate("(let ((x 5)) x)"); + const auto *int2 = engine.get_if(&result2); + if (int2 == nullptr || *int2 != 5) { return false; } + + // Test define identifier fixing + auto result3 = engine.evaluate("(define foo 42) foo"); + const auto *int3 = engine.get_if(&result3); + return int3 != nullptr && *int3 == 42; + }; + STATIC_CHECK(test_fix_identifiers()); +} + +TEST_CASE("Division operator and edge cases", "[division]") +{ + SECTION("Basic division") + { + STATIC_CHECK(evaluate_to("(/ 10 2)") == 5); + STATIC_CHECK(evaluate_to("(/ 100 5)") == 20); + STATIC_CHECK(evaluate_to("(/ -10 2)") == -5); + } + + SECTION("Floating point division") + { + STATIC_CHECK(evaluate_to("(/ 10.0 2.0)") == 5.0); + STATIC_CHECK(evaluate_to("(/ 1.0 3.0)") == 1.0 / 3.0); + } +} + +TEST_CASE("Advanced number parsing edge cases", "[parsing]") +{ + SECTION("Lone minus sign") { STATIC_CHECK(evaluate_to("(error? (-))") == true); } + + SECTION("Scientific notation with negative exponent") + { + STATIC_CHECK(evaluate_to("1e-3") == 0.001); + STATIC_CHECK(evaluate_to("2.5e-2") == 0.025); + } + + SECTION("Invalid number formats") + { + STATIC_CHECK(evaluate_to("(error? 1e)") == true); + STATIC_CHECK(evaluate_to("(error? 1.2.3)") == true); + } +} + +TEST_CASE("Quote handling in various contexts", "[quotes]") +{ + SECTION("Nested quotes") + { + constexpr auto test_nested_quotes = []() { + // ''x evaluates to '(quote x) which is (quote (quote x)) + return evaluate_to("(list? ''x)") == true; + }; + STATIC_CHECK(test_nested_quotes()); + } + + SECTION("Quote with boolean literals") + { + constexpr auto test_quote_booleans = []() { + return evaluate_to("(list? 'true)") == false && evaluate_to("(list? 'false)") == false; + }; + STATIC_CHECK(test_quote_booleans()); + } + + SECTION("Quote with strings") + { + constexpr auto test_quote_strings = []() { return evaluate_to("(list? '\"hello\")") == false; }; + STATIC_CHECK(test_quote_strings()); + } +} + +TEST_CASE("String escape sequence comprehensive tests", "[strings]") +{ + SECTION("All escape sequences") + { + STATIC_CHECK(evaluate_expected(R"("\"")", "\"")); + STATIC_CHECK(evaluate_expected(R"("\\")", "\\")); + STATIC_CHECK(evaluate_expected(R"("\n")", "\n")); + STATIC_CHECK(evaluate_expected(R"("\t")", "\t")); + STATIC_CHECK(evaluate_expected(R"("\r")", "\r")); + STATIC_CHECK(evaluate_expected(R"("\f")", "\f")); + STATIC_CHECK(evaluate_expected(R"("\b")", "\b")); + } + + SECTION("Invalid escape sequences") + { + STATIC_CHECK(evaluate_to(R"((error? "\x"))") == true); + STATIC_CHECK(evaluate_to(R"((error? "\"))") == true); + } +} + +TEST_CASE("Closure operations and edge cases", "[closures]") +{ + SECTION("Closure equality") + { + constexpr auto test_closure_equality = []() { + lefticus::cons_expr engine; + [[maybe_unused]] auto result1 = engine.evaluate("(define f (lambda (x) x))"); + [[maybe_unused]] auto result2 = engine.evaluate("(define g (lambda (x) x))"); + auto result = engine.evaluate("(== f g)"); + const auto *bool_ptr = engine.get_if(&result); + return bool_ptr != nullptr && *bool_ptr == false; + }; + STATIC_CHECK(test_closure_equality()); + + constexpr auto test_closure_equality_2 = []() { + lefticus::cons_expr engine; + [[maybe_unused]] auto result1 = engine.evaluate("(define f (lambda (x) x))"); + auto result = engine.evaluate("(== f f)"); + const auto *bool_ptr = engine.get_if(&result); + return bool_ptr != nullptr && *bool_ptr == true; + }; + STATIC_CHECK(test_closure_equality_2()); + } +} + +TEST_CASE("Whitespace and token parsing edge cases", "[tokenization]") +{ + SECTION("CR/LF combinations") { STATIC_CHECK(evaluate_to("(+\r\n1\r\n2)") == 3); } + + SECTION("Mixed parentheses and quotes") + { + constexpr auto test_quote_parens = []() { return evaluate_to("(list? '())") == true; }; + STATIC_CHECK(test_quote_parens()); + } +} diff --git a/test/error_handling_tests.cpp b/test/error_handling_tests.cpp new file mode 100644 index 0000000..d1d7229 --- /dev/null +++ b/test/error_handling_tests.cpp @@ -0,0 +1,105 @@ +#include + +#include +#include +#include +#include + +using IntType = int; +using FloatType = double; + +namespace { +template constexpr Result evaluate_to(std::string_view input) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value(); +} + +template constexpr bool evaluate_expected(std::string_view input, auto result) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; +} + +// Helper to check if an expression results in an error +constexpr bool is_error(std::string_view input) +{ + lefticus::cons_expr evaluator; + auto result = evaluator.evaluate(input); + return std::holds_alternative>(result.value); +} +}// namespace + +TEST_CASE("Error handling in diverse contexts", "[error]") +{ + // Test the error? predicate + STATIC_CHECK(evaluate_to("(error? (car '()))") == true); + STATIC_CHECK(evaluate_to("(error? 42)") == false); + STATIC_CHECK(evaluate_to("(error? \"hello\")") == false); + STATIC_CHECK(evaluate_to("(error? (lambda (x) x))") == false); + + // Test various error cases + STATIC_CHECK(is_error("(+ 1 \"string\")"));// Type mismatch + STATIC_CHECK(is_error("undefined-var"));// Undefined identifier + STATIC_CHECK(is_error("(+ 1)"));// Wrong number of arguments + STATIC_CHECK(is_error("(42 1 2 3)"));// Invalid function call +} + +TEST_CASE("List bounds checking and error conditions", "[error][list]") +{ + // Test car on empty list + STATIC_CHECK(is_error("(car '())")); + STATIC_CHECK(evaluate_to("(error? (car '()))") == true); + + // Test cdr on empty list (now also returns error) + STATIC_CHECK(is_error("(cdr '())")); + STATIC_CHECK(evaluate_to("(error? (cdr '()))") == true); + + // Test car on non-list types + STATIC_CHECK(is_error("(car 42)")); + STATIC_CHECK(is_error("(car \"string\")")); + STATIC_CHECK(is_error("(car true)")); + STATIC_CHECK(is_error("(car 'symbol)"));// symbols are not lists + + // Test cdr on non-list types + STATIC_CHECK(is_error("(cdr 42)")); + STATIC_CHECK(is_error("(cdr \"string\")")); + STATIC_CHECK(is_error("(cdr true)")); + STATIC_CHECK(is_error("(cdr 'symbol)"));// symbols are not lists +} + +TEST_CASE("Type mismatch error handling", "[error][type]") +{ + // Test different type mismatches + STATIC_CHECK(is_error("(+ 5 \"hello\")"));// Number expected but got string + STATIC_CHECK(is_error("(and true 42)"));// Boolean expected but got number + STATIC_CHECK(is_error("(car 42)"));// List expected but got atom + STATIC_CHECK(is_error("(apply 42 '(1 2 3))"));// Function expected but got number +} + +TEST_CASE("Error propagation in nested expressions", "[error][propagation]") +{ + // Error in argument evaluation should propagate + STATIC_CHECK(is_error("(+ (undefined-var) 5)")); +} + +TEST_CASE("Error handling in get_list and get_list_range", "[error][helper]") +{ + // Test errors in function calls requiring specific list structures + STATIC_CHECK(is_error("(cond 42)"));// cond requires list clauses + STATIC_CHECK(is_error("(let 42 body)"));// let requires binding pairs + STATIC_CHECK(is_error("(define)"));// define requires identifier and value + STATIC_CHECK(is_error("(let ((x)) x)"));// Malformed let bindings +} + +TEST_CASE("Lambda parameter validation", "[error][lambda]") +{ + // Lambda with no body + STATIC_CHECK(is_error("(lambda (x))")); + + // Invalid parameter list + STATIC_CHECK(is_error("(lambda 42 body)")); + + // Calling lambda with wrong number of args + STATIC_CHECK(is_error("((lambda (x y) (+ x y)) 1)")); +} diff --git a/test/list_construction_tests.cpp b/test/list_construction_tests.cpp new file mode 100644 index 0000000..2be69cb --- /dev/null +++ b/test/list_construction_tests.cpp @@ -0,0 +1,146 @@ +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +namespace { +template constexpr Result evaluate_to(std::string_view input) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value(); +} + +template constexpr bool evaluate_expected(std::string_view input, auto result) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; +} +}// namespace + +TEST_CASE("Cons function with various types", "[list][cons]") +{ + // Basic cons with a number and a list + STATIC_CHECK(evaluate_to("(== (cons 1 '(2 3)) '(1 2 3))") == true); + + // Cons with a string + STATIC_CHECK(evaluate_to("(== (cons \"hello\" '(\"world\")) '(\"hello\" \"world\"))") == true); + + // Cons with a boolean + STATIC_CHECK(evaluate_to("(== (cons true '(false)) '(true false))") == true); + + // Cons with a symbol + STATIC_CHECK(evaluate_to("(== (cons 'a '(b c)) '(a b c))") == true); + + // Cons with an empty list + STATIC_CHECK(evaluate_to("(== (cons 1 '()) '(1))") == true); + + // Cons with a nested list + STATIC_CHECK(evaluate_to("(== (cons '(1 2) '(3 4)) '((1 2) 3 4))") == true); +} + +TEST_CASE("Append function with various lists", "[list][append]") +{ + // Basic append with two simple lists + STATIC_CHECK(evaluate_to("(== (append '(1 2) '(3 4)) '(1 2 3 4))") == true); + + // Append with an empty first list + STATIC_CHECK(evaluate_to("(== (append '() '(1 2)) '(1 2))") == true); + + // Append with an empty second list + STATIC_CHECK(evaluate_to("(== (append '(1 2) '()) '(1 2))") == true); + + // Append with two empty lists + STATIC_CHECK(evaluate_to("(== (append '() '()) '())") == true); + + // Append with nested lists + STATIC_CHECK(evaluate_to("(== (append '((1) 2) '(3 (4))) '((1) 2 3 (4)))") == true); + + // Append with mixed content + STATIC_CHECK(evaluate_to("(== (append '(1 \"two\") '(true 3.0)) '(1 \"two\" true 3.0))") == true); +} + +TEST_CASE("Car function with various lists", "[list][car]") +{ + // Basic car of a simple list + STATIC_CHECK(evaluate_to("(car '(1 2 3))") == 1); + + // Car of a list with mixed types + STATIC_CHECK(evaluate_expected("(car '(\"hello\" 2 3))", "hello")); + + // Car of a list with a nested list + STATIC_CHECK(evaluate_to("(== (car '((1 2) 3 4)) '(1 2))") == true); + + // Car of a single-element list + STATIC_CHECK(evaluate_to("(car '(42))") == 42); + + // Car of a quoted symbol list + STATIC_CHECK(evaluate_to("(== (car '(a b c)) 'a)") == true); +} + +TEST_CASE("Cdr function with various lists", "[list][cdr]") +{ + // Basic cdr of a simple list + STATIC_CHECK(evaluate_to("(== (cdr '(1 2 3)) '(2 3))") == true); + + // Cdr of a list with mixed types + STATIC_CHECK(evaluate_to("(== (cdr '(\"hello\" 2 3)) '(2 3))") == true); + + // Cdr of a list with a nested list + STATIC_CHECK(evaluate_to("(== (cdr '((1 2) 3 4)) '(3 4))") == true); + + // Cdr of a single-element list returns empty list + STATIC_CHECK(evaluate_to("(== (cdr '(42)) '())") == true); + + // Cdr of a two-element list + STATIC_CHECK(evaluate_to("(== (cdr '(1 2)) '(2))") == true); +} + +TEST_CASE("Complex list construction", "[list][complex]") +{ + // Combining cons, car, and cdr + STATIC_CHECK(evaluate_to(R"( + (== (cons (car '(1 2)) + (cdr '(3 4 5))) + '(1 4 5)) + )") == true); + + // Nested cons calls + STATIC_CHECK(evaluate_to(R"( + (== (cons 1 (cons 2 (cons 3 '()))) + '(1 2 3)) + )") == true); + + // Combining append with cons + STATIC_CHECK(evaluate_to(R"( + (== (append (cons 1 '(2)) + (cons 3 '(4))) + '(1 2 3 4)) + )") == true); + + // Building complex nested structures + STATIC_CHECK(evaluate_to(R"( + (== (cons (cons 1 '(2)) + (cons (cons 3 '(4)) + '())) + '((1 2) (3 4))) + )") == true); +} + +TEST_CASE("List construction edge cases", "[list][edge]") +{ + // Cons with both arguments being lists + STATIC_CHECK(evaluate_to("(== (cons '(1) '(2)) '((1) 2))") == true); + + // Nested empty lists + STATIC_CHECK(evaluate_to("(== (cons '() '()) '(()))") == true); + + // Triple-nested cons + STATIC_CHECK(evaluate_to(R"( + (== (cons 1 (cons 2 (cons 3 '()))) + '(1 2 3)) + )") == true); +} diff --git a/test/list_tests.cpp b/test/list_tests.cpp index 157c501..829628c 100644 --- a/test/list_tests.cpp +++ b/test/list_tests.cpp @@ -1,13 +1,13 @@ #include -#include #include -#include -#include +#include +#include using IntType = int; using FloatType = double; +namespace { template constexpr Result evaluate_to(std::string_view input) { lefticus::cons_expr evaluator; @@ -19,6 +19,7 @@ template constexpr bool evaluate_expected(std::string_view inpu lefticus::cons_expr evaluator; return evaluator.evaluate_to(input).value() == result; } +}// namespace // Basic List Creation Tests TEST_CASE("Basic list creation", "[lists]") @@ -225,15 +226,4 @@ TEST_CASE("List manipulation algorithms", "[lists][algorithms]") (simple-fn '()) )") == true); - - // Create a list of numbers using do - STATIC_CHECK(evaluate_to(R"( - (define make-list - (lambda (n) - (do ((i n (- i 1)) - (result '() (cons i result))) - ((<= i 0) result)))) - - (== (make-list 3) '(1 2 3)) - )") == true); -} \ No newline at end of file +} diff --git a/test/parser_tests.cpp b/test/parser_tests.cpp index d1e296b..18bb04f 100644 --- a/test/parser_tests.cpp +++ b/test/parser_tests.cpp @@ -1,44 +1,29 @@ +#include #include -#include #include -#include -#include -#include +#include + +#include +#include using IntType = int; using FloatType = double; -// Helper function for getting values from parsing without evaluation -template constexpr Result parse_result(std::string_view input) -{ - lefticus::cons_expr evaluator; - auto [parsed, _] = evaluator.parse(input); - - const auto *list = std::get_if::list_type>(&parsed.value); - if (list != nullptr && list->size == 1) { - // Extract the first element from the parsed list - const auto *result = evaluator.template get_if(&evaluator.values[(*list)[0]]); - if (result != nullptr) { return *result; } - } - - // This is a fallback that will cause the test to fail if we can't extract the expected type - return Result{}; -} +using evaluator_type = lefticus::cons_expr; -// Helper function for checking if a parsed expression contains a specific type -template constexpr bool is_of_type(std::string_view input) +namespace { +template constexpr auto parse(std::basic_string_view str) { - lefticus::cons_expr evaluator; - auto [parsed, _] = evaluator.parse(input); + evaluator_type evaluator; + const auto list = evaluator.parse(str).first; - const auto *list = std::get_if::list_type>(&parsed.value); - if (list != nullptr && list->size == 1) { - return evaluator.template get_if(&evaluator.values[(*list)[0]]) != nullptr; - } + if (list.size != 1) { throw "expected exactly one thing parsed"; }// NOLINT - return false; + return evaluator.values[list[0]]; } +}// namespace + // Basic Tokenization Tests TEST_CASE("Basic tokenization", "[parser][tokenize]") @@ -48,42 +33,42 @@ TEST_CASE("Basic tokenization", "[parser][tokenize]") using Token = lefticus::Token; // Simple tokens - Token token1 = lefticus::next_token(std::string_view("hello")); - if (token1.parsed != std::string_view("hello")) return false; + Token const token1 = lefticus::next_token(std::string_view("hello")); + if (token1.parsed != std::string_view("hello")) { return false; } // Whitespace handling - Token token2 = lefticus::next_token(std::string_view(" hello")); - if (token2.parsed != std::string_view("hello")) return false; + Token const token2 = lefticus::next_token(std::string_view(" hello")); + if (token2.parsed != std::string_view("hello")) { return false; } - Token token3 = lefticus::next_token(std::string_view("hello ")); - if (token3.parsed != std::string_view("hello")) return false; + Token const token3 = lefticus::next_token(std::string_view("hello ")); + if (token3.parsed != std::string_view("hello")) { return false; } // Multiple tokens - Token token4 = lefticus::next_token(std::string_view("hello world")); - if (token4.parsed != std::string_view("hello") || token4.remaining != std::string_view("world")) return false; + Token const token4 = lefticus::next_token(std::string_view("hello world")); + if (token4.parsed != std::string_view("hello") || token4.remaining != std::string_view("world")) { return false; } // Parentheses - Token token5 = lefticus::next_token(std::string_view("(hello)")); - if (token5.parsed != std::string_view("(") || token5.remaining != std::string_view("hello)")) return false; + Token const token5 = lefticus::next_token(std::string_view("(hello)")); + if (token5.parsed != std::string_view("(") || token5.remaining != std::string_view("hello)")) { return false; } - Token token6 = lefticus::next_token(std::string_view(")hello")); - if (token6.parsed != std::string_view(")") || token6.remaining != std::string_view("hello")) return false; + Token const token6 = lefticus::next_token(std::string_view(")hello")); + if (token6.parsed != std::string_view(")") || token6.remaining != std::string_view("hello")) { return false; } // Quote syntax - Token token7 = lefticus::next_token(std::string_view("'(hello)")); - if (token7.parsed != std::string_view("'(") || token7.remaining != std::string_view("hello)")) return false; + Token const token7 = lefticus::next_token(std::string_view("'(hello)")); + if (token7.parsed != std::string_view("'") || token7.remaining != std::string_view("(hello)")) { return false; } // Strings - Token token8 = lefticus::next_token(std::string_view("\"hello\"")); - if (token8.parsed != std::string_view("\"hello\"")) return false; + Token const token8 = lefticus::next_token(std::string_view("\"hello\"")); + if (token8.parsed != std::string_view("\"hello\"")) { return false; } // Empty input - Token token9 = lefticus::next_token(std::string_view("")); - if (!token9.parsed.empty() || !token9.remaining.empty()) return false; + Token const token9 = lefticus::next_token(std::string_view("")); + if (!token9.parsed.empty() || !token9.remaining.empty()) { return false; } // Comments - Token token10 = lefticus::next_token(std::string_view("; comment\nhello")); - if (token10.parsed != std::string_view("hello")) return false; + Token const token10 = lefticus::next_token(std::string_view("; comment\nhello")); + if (token10.parsed != std::string_view("hello")) { return false; } return true; }; @@ -143,7 +128,7 @@ TEST_CASE("Token sequence processing", "[parser][token-sequence]") // Let's ignore the exact amount of whitespace return token6.parsed == std::string_view("(") && (token6.remaining.find("quote") != std::string_view::npos) && (token6.remaining.find("hello") != std::string_view::npos) - && (token6.remaining.find(")") != std::string_view::npos); + && (token6.remaining.find(')') != std::string_view::npos); }; // Check all individual assertions @@ -188,7 +173,7 @@ TEST_CASE("Whitespace handling", "[parser][whitespace]") // The returned remaining string likely has normalized whitespace // Let's ignore the exact amount of whitespace return token5.parsed == std::string_view("(") && (token5.remaining.find("hello") != std::string_view::npos) - && (token5.remaining.find(")") != std::string_view::npos); + && (token5.remaining.find(')') != std::string_view::npos); }; // Only whitespace @@ -268,8 +253,8 @@ TEST_CASE("String parsing", "[parser][strings]") // String with escaped quote constexpr auto test_string4 = []() { - auto token4 = lefticus::next_token(std::string_view("\"hello\\\"world\"")); - return token4.parsed == std::string_view("\"hello\\\"world\""); + auto token4 = lefticus::next_token(std::string_view(R"("hello\"world")")); + return token4.parsed == std::string_view(R"("hello\"world")"); }; // String followed by other tokens @@ -286,45 +271,102 @@ TEST_CASE("String parsing", "[parser][strings]") STATIC_CHECK(test_string5()); } -// Number Parsing Tests -TEST_CASE("Number parsing", "[parser][numbers]") +TEST_CASE("String parse failures", "[parser][strings][escapes]") { - constexpr auto test_int_parsing = []() { - // Integer parsing - auto [success1, value1] = lefticus::parse_number(std::string_view("123")); - if (!success1 || value1 != 123) return false; + STATIC_CHECK(std::holds_alternative(parse(std::string_view(R"("\q")")).value)); + STATIC_CHECK(std::holds_alternative(parse(std::string_view(R"("\q)")).value)); +} - auto [success2, value2] = lefticus::parse_number(std::string_view("-456")); - if (!success2 || value2 != -456) return false; +// String Escape Character Tests +TEST_CASE("String escape characters", "[parser][strings][escapes]") +{ + // Escaped double quote + constexpr auto test_escaped_quote = []() { + lefticus::cons_expr evaluator; + auto [parsed, _] = evaluator.parse(R"("Quote: \"Hello\"")"); - auto [success3, value3] = lefticus::parse_number(std::string_view("not_a_number")); - if (success3) return false;// Should fail + // Extract the string content + if (parsed.size != 1) { return false; } + + const auto *atom = std::get_if::Atom>(&evaluator.values[parsed[0]].value); + if (atom == nullptr) { return false; } + + const auto *string_val = std::get_if::string_type>(atom); + if (string_val == nullptr) { return false; } + + // Check the raw tokenized string includes the escapes + auto token = lefticus::next_token(std::string_view(R"("Quote: \"Hello\"")")); + if (token.parsed != std::string_view(R"("Quote: \"Hello\"")")) { return false; } return true; }; - constexpr auto test_float_parsing = []() { - // Float parsing - auto [success1, value1] = lefticus::parse_number(std::string_view("123.456")); - if (!success1 || std::abs(value1 - 123.456) > 0.0001) return false; + // Escaped backslash + constexpr auto test_escaped_backslash = []() { + auto token = lefticus::next_token(std::string_view(R"("Backslash: \\")")); + return token.parsed == std::string_view(R"("Backslash: \\")"); + }; + + // Multiple escape sequences + constexpr auto test_multiple_escapes = []() { + auto token = lefticus::next_token(std::string_view(R"("Escapes: \\ \" \n \t \r")")); + return token.parsed == std::string_view(R"("Escapes: \\ \" \n \t \r")"); + }; + + // Escape at end of string + constexpr auto test_escape_at_end = []() { + auto token = lefticus::next_token(std::string_view(R"("Escape at end: \")")); + return token.parsed == std::string_view(R"("Escape at end: \")"); + }; + + // Unterminated string with escape + constexpr auto test_unterminated_escape = []() { + auto token = lefticus::next_token(std::string_view("\"Unterminated \\")); + return token.parsed == std::string_view("\"Unterminated \\"); + }; + + // Common escape sequences: \n \t \r \f \b + constexpr auto test_common_escapes = []() { + auto token = lefticus::next_token(std::string_view(R"("Special chars: \n\t\r\f\b")")); + return token.parsed == std::string_view(R"("Special chars: \n\t\r\f\b")"); + }; - auto [success2, value2] = lefticus::parse_number(std::string_view("-789.012")); - if (!success2 || std::abs(value2 - (-789.012)) > 0.0001) return false; + // Test handling of consecutive escapes + constexpr auto test_consecutive_escapes = []() { + auto token = lefticus::next_token(std::string_view(R"("Double escapes: \\\"")")); + return token.parsed == std::string_view(R"("Double escapes: \\\"")"); + }; - auto [success3, value3] = lefticus::parse_number(std::string_view("1e3")); - if (!success3 || std::abs(value3 - 1000.0) > 0.0001) return false; + // Check all individual assertions + STATIC_CHECK(test_escaped_quote()); + STATIC_CHECK(test_escaped_backslash()); + STATIC_CHECK(test_multiple_escapes()); + STATIC_CHECK(test_escape_at_end()); + STATIC_CHECK(test_unterminated_escape()); + STATIC_CHECK(test_common_escapes()); + STATIC_CHECK(test_consecutive_escapes()); +} - auto [success4, value4] = lefticus::parse_number(std::string_view("1.5e-2")); - if (!success4 || std::abs(value4 - 0.015) > 0.0001) return false; +// Number Parsing Tests +TEST_CASE("Number parsing", "[parser][numbers]") +{ + constexpr auto test_int_parsing = []() { + // Integer parsing + auto [success1, value1] = lefticus::parse_number(std::string_view("123")); + if (!success1 || value1 != 123) { return false; } - auto [success5, value5] = lefticus::parse_number(std::string_view("not_a_number")); - if (success5) return false;// Should fail + auto [success2, value2] = lefticus::parse_number(std::string_view("-456")); + if (!success2 || value2 != -456) { return false; } + + auto [success3, value3] = lefticus::parse_number(std::string_view("not_a_number")); + if (success3) { + return false;// Should fail + } return true; }; STATIC_CHECK(test_int_parsing()); - STATIC_CHECK(test_float_parsing()); } // List Structure Tests @@ -335,15 +377,14 @@ TEST_CASE("List structure", "[parser][lists]") lefticus::cons_expr evaluator; // Parse an empty list: () - auto [parsed_result, _] = evaluator.parse(std::string_view("()")); + auto [outer_list, _] = evaluator.parse(std::string_view("()")); // Parse always returns a list containing the parsed expressions // For an empty list, we expect a list with one item (which is itself an empty list) - const auto *outer_list = std::get_if::list_type>(&parsed_result.value); - if (outer_list == nullptr || outer_list->size != 1) return false; + if (outer_list.size != 1) { return false; } // Check that the inner element is an empty list - const auto &inner_elem = evaluator.values[(*outer_list)[0]]; + const auto &inner_elem = evaluator.values[outer_list[0]]; const auto *inner_list = std::get_if::list_type>(&inner_elem.value); return inner_list != nullptr && inner_list->size == 0; }; @@ -353,14 +394,13 @@ TEST_CASE("List structure", "[parser][lists]") lefticus::cons_expr evaluator; // Parse a simple list with three elements: (a b c) - auto [parsed_result, _] = evaluator.parse(std::string_view("(a b c)")); + auto [outer_list, _] = evaluator.parse(std::string_view("(a b c)")); // Outer list should contain one item - const auto *outer_list = std::get_if::list_type>(&parsed_result.value); - if (outer_list == nullptr || outer_list->size != 1) return false; + if (outer_list.size != 1) { return false; } // Inner list should contain three elements (a, b, c) - const auto &inner_elem = evaluator.values[(*outer_list)[0]]; + const auto &inner_elem = evaluator.values[outer_list[0]]; const auto *inner_list = std::get_if::list_type>(&inner_elem.value); return inner_list != nullptr && inner_list->size == 3; }; @@ -370,16 +410,15 @@ TEST_CASE("List structure", "[parser][lists]") lefticus::cons_expr evaluator; // Parse a list with a nested list: (a (b c) d) - auto [parsed_result, _] = evaluator.parse(std::string_view("(a (b c) d)")); + auto [outer_list, _] = evaluator.parse(std::string_view("(a (b c) d)")); // Outer list should contain one item - const auto *outer_list = std::get_if::list_type>(&parsed_result.value); - if (outer_list == nullptr || outer_list->size != 1) return false; + if (outer_list.size != 1) { return false; } // Inner list should contain three elements: a, (b c), d - const auto &inner_elem = evaluator.values[(*outer_list)[0]]; + const auto &inner_elem = evaluator.values[outer_list[0]]; const auto *inner_list = std::get_if::list_type>(&inner_elem.value); - if (inner_list == nullptr || inner_list->size != 3) return false; + if (inner_list == nullptr || inner_list->size != 3) { return false; } // The second element should be a nested list with 2 elements: b, c const auto &nested_elem = evaluator.values[(*inner_list)[1]]; @@ -393,70 +432,6 @@ TEST_CASE("List structure", "[parser][lists]") STATIC_CHECK(test_nested_list()); } -// Quote Syntax Tests -TEST_CASE("Quote syntax", "[parser][quotes]") -{ - constexpr auto test_quotes = []() { - lefticus::cons_expr evaluator; - - // Quoted symbol - auto [quoted_symbol, _1] = evaluator.parse("'symbol"); - const auto *list1 = std::get_if::list_type>("ed_symbol.value); - if (list1 == nullptr || list1->size != 1) return false; - - auto &first_item = evaluator.values[(*list1)[0]]; - const auto *atom = std::get_if::Atom>(&first_item.value); - if (atom == nullptr) return false; - if (std::get_if::symbol_type>(atom) == nullptr) return false; - - // Quoted list - auto [quoted_list, _2] = evaluator.parse("'(a b c)"); - const auto *list2 = std::get_if::list_type>("ed_list.value); - if (list2 == nullptr || list2->size != 1) return false; - - const auto *literal_list = - std::get_if::literal_list_type>(&evaluator.values[(*list2)[0]].value); - if (literal_list == nullptr || literal_list->items.size != 3) return false; - - return true; - }; - - STATIC_CHECK(test_quotes()); -} - -// Symbol vs Identifier Tests -TEST_CASE("Symbol vs identifier", "[parser][symbols]") -{ - constexpr auto test_symbol_vs_identifier = []() { - lefticus::cons_expr evaluator; - - // Symbol (quoted identifier) - auto [symbol_expr, _1] = evaluator.parse("'symbol"); - const auto *list1 = std::get_if::list_type>(&symbol_expr.value); - if (list1 == nullptr || list1->size != 1) return false; - - const auto *atom1 = std::get_if::Atom>(&evaluator.values[(*list1)[0]].value); - if (atom1 == nullptr) return false; - - const auto *symbol = std::get_if::symbol_type>(atom1); - if (symbol == nullptr) return false; - - // Regular identifier - auto [id_expr, _2] = evaluator.parse("identifier"); - const auto *list2 = std::get_if::list_type>(&id_expr.value); - if (list2 == nullptr || list2->size != 1) return false; - - const auto *atom2 = std::get_if::Atom>(&evaluator.values[(*list2)[0]].value); - if (atom2 == nullptr) return false; - - const auto *identifier = std::get_if::identifier_type>(atom2); - if (identifier == nullptr) return false; - - return true; - }; - - STATIC_CHECK(test_symbol_vs_identifier()); -} // Boolean Literal Tests TEST_CASE("Boolean literals", "[parser][booleans]") @@ -466,25 +441,23 @@ TEST_CASE("Boolean literals", "[parser][booleans]") // Parse true auto [true_expr, _1] = evaluator.parse("true"); - const auto *list1 = std::get_if::list_type>(&true_expr.value); - if (list1 == nullptr || list1->size != 1) return false; + if (true_expr.size != 1) { return false; } - const auto *atom1 = std::get_if::Atom>(&evaluator.values[(*list1)[0]].value); - if (atom1 == nullptr) return false; + const auto *atom1 = std::get_if::Atom>(&evaluator.values[true_expr[0]].value); + if (atom1 == nullptr) { return false; } const auto *bool_val1 = std::get_if(atom1); - if (bool_val1 == nullptr || !(*bool_val1)) return false; + if (bool_val1 == nullptr || !(*bool_val1)) { return false; } // Parse false auto [false_expr, _2] = evaluator.parse("false"); - const auto *list2 = std::get_if::list_type>(&false_expr.value); - if (list2 == nullptr || list2->size != 1) return false; + if (false_expr.size != 1) { return false; } - const auto *atom2 = std::get_if::Atom>(&evaluator.values[(*list2)[0]].value); - if (atom2 == nullptr) return false; + const auto *atom2 = std::get_if::Atom>(&evaluator.values[false_expr[0]].value); + if (atom2 == nullptr) { return false; } const auto *bool_val2 = std::get_if(atom2); - if (bool_val2 == nullptr || (*bool_val2)) return false; + if (bool_val2 == nullptr || (*bool_val2)) { return false; } return true; }; @@ -502,11 +475,10 @@ TEST_CASE("Multiple expressions", "[parser][multiple]") auto [parsed, _] = evaluator.parse(std::string_view("(define x 10)")); // Outer list should contain one item - const auto *outer_list = std::get_if::list_type>(&parsed.value); - if (outer_list == nullptr || outer_list->size != 1) return false; + if (parsed.size != 1) { return false; } // Inner list should contain three elements: define, x, 10 - const auto &inner_elem = evaluator.values[(*outer_list)[0]]; + const auto &inner_elem = evaluator.values[parsed[0]]; const auto *inner_list = std::get_if::list_type>(&inner_elem.value); return inner_list != nullptr && inner_list->size == 3; @@ -525,18 +497,17 @@ TEST_CASE("Complex expressions", "[parser][complex]") auto [parsed, _] = evaluator.parse(std::string_view("(lambda (x) (+ x 1))")); // Outer list should contain one item - const auto *outer_list = std::get_if::list_type>(&parsed.value); - if (outer_list == nullptr || outer_list->size != 1) return false; + if (parsed.size != 1) { return false; } // Inner list should contain three elements: lambda, (x), (+ x 1) - const auto &inner_elem = evaluator.values[(*outer_list)[0]]; + const auto &inner_elem = evaluator.values[parsed[0]]; const auto *inner_list = std::get_if::list_type>(&inner_elem.value); - if (inner_list == nullptr || inner_list->size != 3) return false; + if (inner_list == nullptr || inner_list->size != 3) { return false; } // Second element should be a parameter list containing just x const auto ¶ms = evaluator.values[(*inner_list)[1]]; const auto *params_list = std::get_if::list_type>(¶ms.value); - if (params_list == nullptr || params_list->size != 1) return false; + if (params_list == nullptr || params_list->size != 1) { return false; } return true; }; @@ -552,17 +523,16 @@ TEST_CASE("String content", "[parser][string-content]") // Parse a string and check its content auto [string_expr, _] = evaluator.parse("\"hello world\""); - const auto *list = std::get_if::list_type>(&string_expr.value); - if (list == nullptr || list->size != 1) return false; + if (string_expr.size != 1) { return false; } - const auto *atom = std::get_if::Atom>(&evaluator.values[(*list)[0]].value); - if (atom == nullptr) return false; + const auto *atom = std::get_if::Atom>(&evaluator.values[string_expr[0]].value); + if (atom == nullptr) { return false; } const auto *string_val = std::get_if::string_type>(atom); - if (string_val == nullptr) return false; + if (string_val == nullptr) { return false; } - auto sv = evaluator.strings.view(*string_val); - if (sv != std::string_view("hello world")) return false; + auto found_string = evaluator.strings.view(*string_val); + if (found_string != std::string_view("hello world")) { return false; } return true; }; @@ -580,18 +550,17 @@ TEST_CASE("Mixed content", "[parser][mixed]") auto [mixed_expr, _] = evaluator.parse(std::string_view("(list 123 \"hello\" true 'symbol (nested))")); // Outer list should contain one item - const auto *outer_list = std::get_if::list_type>(&mixed_expr.value); - if (outer_list == nullptr || outer_list->size != 1) return false; + if (mixed_expr.size != 1) { return false; } // Inner list should contain six elements: list, 123, "hello", true, 'symbol, (nested) - const auto &inner_elem = evaluator.values[(*outer_list)[0]]; + const auto &inner_elem = evaluator.values[mixed_expr[0]]; const auto *inner_list = std::get_if::list_type>(&inner_elem.value); - if (inner_list == nullptr || inner_list->size != 6) return false; + if (inner_list == nullptr || inner_list->size != 6) { return false; } // First element should be an identifier "list" const auto &first_elem = evaluator.values[(*inner_list)[0]]; const auto *first_atom = std::get_if::Atom>(&first_elem.value); - if (first_atom == nullptr) return false; + if (first_atom == nullptr) { return false; } const auto *id = std::get_if::identifier_type>(first_atom); return id != nullptr; @@ -600,67 +569,105 @@ TEST_CASE("Mixed content", "[parser][mixed]") STATIC_CHECK(test_mixed_content()); } -// Quoted List Tests -TEST_CASE("Quoted lists", "[parser][quoted-lists]") -{ - constexpr auto test_quoted_lists = []() { - lefticus::cons_expr evaluator; - - // Empty quoted list - auto [empty, _1] = evaluator.parse("'()"); - const auto *list1 = std::get_if::list_type>(&empty.value); - if (list1 == nullptr || list1->size != 1) return false; - - const auto *literal_list1 = - std::get_if::literal_list_type>(&evaluator.values[(*list1)[0]].value); - if (literal_list1 == nullptr || literal_list1->items.size != 0) return false; - - // Simple quoted list - auto [simple, _2] = evaluator.parse("'(1 2 3)"); - const auto *list2 = std::get_if::list_type>(&simple.value); - if (list2 == nullptr || list2->size != 1) return false; - - const auto *literal_list2 = - std::get_if::literal_list_type>(&evaluator.values[(*list2)[0]].value); - if (literal_list2 == nullptr || literal_list2->items.size != 3) return false; - - // Nested quoted list - auto [nested, _3] = evaluator.parse("'(1 (2 3) 4)"); - const auto *list3 = std::get_if::list_type>(&nested.value); - if (list3 == nullptr || list3->size != 1) return false; - - const auto *literal_list3 = - std::get_if::literal_list_type>(&evaluator.values[(*list3)[0]].value); - if (literal_list3 == nullptr || literal_list3->items.size != 3) return false; - - return true; - }; - - STATIC_CHECK(test_quoted_lists()); -} - // Special Character Tests TEST_CASE("Special characters", "[parser][special-chars]") { constexpr auto test_special_chars = []() { // Various identifier formats with special characters auto token1 = lefticus::next_token(std::string_view("hello-world")); - if (token1.parsed != std::string_view("hello-world")) return false; + if (token1.parsed != std::string_view("hello-world")) { return false; } auto token2 = lefticus::next_token(std::string_view("symbol+")); - if (token2.parsed != std::string_view("symbol+")) return false; + if (token2.parsed != std::string_view("symbol+")) { return false; } auto token3 = lefticus::next_token(std::string_view("_special_")); - if (token3.parsed != std::string_view("_special_")) return false; + if (token3.parsed != std::string_view("_special_")) { return false; } auto token4 = lefticus::next_token(std::string_view("*wild*")); - if (token4.parsed != std::string_view("*wild*")) return false; + if (token4.parsed != std::string_view("*wild*")) { return false; } auto token5 = lefticus::next_token(std::string_view("symbol?")); - if (token5.parsed != std::string_view("symbol?")) return false; + if (token5.parsed != std::string_view("symbol?")) { return false; } return true; }; STATIC_CHECK(test_special_chars()); -} \ No newline at end of file +} + +using LongDouble = long double; + +// Number Parsing Edge Cases +TEMPLATE_TEST_CASE("integral parsing", "[parser][numbers][edge]", int, long, short) +{ + STATIC_CHECK(lefticus::parse_number(std::string_view("123x")).first == false); + STATIC_CHECK(lefticus::parse_number(std::string_view("123e4")).first == false); + STATIC_CHECK(lefticus::parse_number(std::string_view("-123")).second == TestType{ -123 }); +} + + +// LCOV_EXCL_START +// Number Parsing Edge Cases +TEMPLATE_TEST_CASE("Floating point parsing", "[parser][numbers][edge]", float, double, LongDouble) +{ + STATIC_CHECK(static_cast(123.456L) == lefticus::parse_number(std::string_view("123.456")).second); + STATIC_CHECK( + static_cast(-789.012L) == lefticus::parse_number(std::string_view("-789.012")).second); + STATIC_CHECK(static_cast(1000.0L) == lefticus::parse_number(std::string_view("1e3")).second); + STATIC_CHECK(static_cast(0.015L) == lefticus::parse_number(std::string_view("1.5e-2")).second); + + STATIC_CHECK(lefticus::parse_number(std::string_view("123.1.")).first == false); + STATIC_CHECK(lefticus::parse_number(std::string_view("123.1e.")).first == false); + STATIC_CHECK(lefticus::parse_number(std::string_view("123.1e")).first == false); + STATIC_CHECK(lefticus::parse_number(std::string_view("123e")).first == false); + STATIC_CHECK(lefticus::parse_number(std::string_view("123e4")).second == static_cast(123e4L)); + STATIC_CHECK(lefticus::parse_number(std::string_view("123ex")).first == false); + STATIC_CHECK(static_cast(.123e2l) == lefticus::parse_number(std::string_view(".123e2")).second); + STATIC_CHECK(static_cast(12.3L) == lefticus::parse_number(std::string_view(".123e2")).second); + STATIC_CHECK( + lefticus::parse_number(std::string_view("123.456e3")).second == static_cast(123456L)); + STATIC_CHECK(static_cast(.123l) == lefticus::parse_number(std::string_view(".123")).second); + STATIC_CHECK(static_cast(1.L) == lefticus::parse_number(std::string_view("1.")).second); +} +// LCOV_EXCL_STOP + +// Branch Coverage Enhancement Tests - Only Missing Cases + +TEST_CASE("Missing number parsing edge cases", "[parser][coverage]") +{ + // Test lone minus sign - this specific case may not be covered + STATIC_CHECK(lefticus::parse_number(std::string_view("-")).first == false); + + // Test lone plus sign + STATIC_CHECK(lefticus::parse_number(std::string_view("+")).first == false); +} + +TEST_CASE("Missing token parsing edge cases", "[parser][coverage]") +{ + // Test carriage return + newline specifically + constexpr auto test_crlf = []() constexpr { + auto token = lefticus::next_token(std::string_view("\r\n token")); + return token.parsed == "token"; + }; + STATIC_CHECK(test_crlf()); + + // Test empty string input + constexpr auto test_empty = []() constexpr { + auto token = lefticus::next_token(std::string_view("")); + return token.parsed.empty(); + }; + STATIC_CHECK(test_empty()); +} + +TEST_CASE("Parser null pointer safety", "[parser][coverage]") +{ + constexpr auto test_null_safety = []() constexpr { + lefticus::cons_expr<> const engine; + + // Test null pointer in get_if + const decltype(engine)::SExpr *null_ptr = nullptr; + const auto *result = engine.get_if(null_ptr); + return result == nullptr; + }; + STATIC_CHECK(test_null_safety()); +} diff --git a/test/recursion_and_closure_tests.cpp b/test/recursion_and_closure_tests.cpp new file mode 100644 index 0000000..846d96c --- /dev/null +++ b/test/recursion_and_closure_tests.cpp @@ -0,0 +1,98 @@ +#include + +#include +#include +#include +#include + +using IntType = int; +using FloatType = double; + +namespace { +template constexpr Result evaluate_to(std::string_view input) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value(); +} + +template constexpr bool evaluate_expected(std::string_view input, auto result) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; +} +}// namespace + +TEST_CASE("Recursive lambda passed to another lambda", "[recursion][closure]") +{ + STATIC_CHECK(evaluate_to(R"( + ; Higher-order function that applies a function n times + (define apply-n-times + (lambda (f n x) + (if (== n 0) + x + (f (apply-n-times f (- n 1) x))))) + + ; Use it to calculate 2^10 + (define double (lambda (x) (* 2 x))) + (apply-n-times double 10 1) + )") == 1024); +} + + +TEST_CASE("Deep recursive function with closure", "[recursion][closure]") +{ + STATIC_CHECK(evaluate_to(R"( + ; Recursive Fibonacci function + (define fibonacci + (lambda (n) + (cond + ((== n 0) 0) + ((== n 1) 1) + (else (+ (fibonacci (- n 1)) + (fibonacci (- n 2))))))) + + (fibonacci 10) + )") == 55); +} + +TEST_CASE("Closure with self-reference error handling", "[recursion][closure][error]") +{ + // Create an evaluator for checking error cases + lefticus::cons_expr evaluator; + + // Test incorrect number of parameters + auto result = evaluator.evaluate(R"( + (define factorial + (lambda (n) + (if (== n 0) + 1 + (* n (factorial (- n 1)))))) + + (factorial 5 10) ; Too many arguments + )"); + + REQUIRE(std::holds_alternative>(result.value)); +} + +TEST_CASE("Complex nested scoping scenarios", "[recursion][closure][scoping]") +{ + STATIC_CHECK(evaluate_to(R"( + (define make-adder + (lambda (x) + (lambda (y) + (+ x y)))) + + (define add5 (make-adder 5)) + (define add10 (make-adder 10)) + + (+ (add5 3) (add10 7)) + )") == 25);// (5+3) + (10+7) + + // More complex nesting with let and lambda + STATIC_CHECK(evaluate_to(R"( + (let ((x 10)) + (let ((f (lambda (y) (+ x y)))) + (let ((x 20)) ; This x should not affect the closure + (f 5)))) + )") == 15);// 10 + 5, not 20 + 5 +} diff --git a/test/recursion_tests.cpp b/test/recursion_tests.cpp index a6fe0c9..cd289b9 100644 --- a/test/recursion_tests.cpp +++ b/test/recursion_tests.cpp @@ -1,13 +1,13 @@ #include -#include #include -#include -#include +#include +#include using IntType = int; using FloatType = double; +namespace { template constexpr Result evaluate_to(std::string_view input) { lefticus::cons_expr evaluator; @@ -19,6 +19,7 @@ template constexpr bool evaluate_expected(std::string_view inpu lefticus::cons_expr evaluator; return evaluator.evaluate_to(input).value() == result; } +}// namespace TEST_CASE("Y-Combinator", "[recursion]") { diff --git a/test/scoping_tests.cpp b/test/scoping_tests.cpp new file mode 100644 index 0000000..34afb68 --- /dev/null +++ b/test/scoping_tests.cpp @@ -0,0 +1,399 @@ +#include + +#include +#include +#include +#include + +using IntType = int; +using FloatType = double; + +namespace { +template constexpr Result evaluate_to(std::string_view input) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value(); +} + +template constexpr bool evaluate_expected(std::string_view input, auto result) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; +} + + +// Function to check if an evaluation fails with an error +constexpr bool expect_error(std::string_view input) +{ + lefticus::cons_expr evaluator; + auto result = evaluator.evaluate(input); + // Check if the result is an error type + return std::holds_alternative::error_type>(result.value); +} +}// namespace + +// ----- Basic Scoping Tests ----- + +TEST_CASE("Basic identifier scoping", "[scoping][basic]") +{ + // Simple undefined identifier - should fail + STATIC_CHECK(expect_error("undefined_variable")); + + // Basic define at global scope + STATIC_CHECK(evaluate_to(R"( + (define x 10) + x + )") == 10); + + // Shadowing a global definition with a local one + STATIC_CHECK(evaluate_to(R"( + (define x 10) + (let ((x 20)) + x) + )") == 20); + + // Outer scope still has original value + STATIC_CHECK(evaluate_to(R"( + (define x 10) + (let ((x 20)) + x) + x + )") == 10); + + // Multiple nestings - innermost wins + STATIC_CHECK(evaluate_to(R"( + (define x 10) + (let ((x 20)) + (let ((x 30)) + x)) + )") == 30); +} + +TEST_CASE("Lambda scoping", "[scoping][lambda]") +{ + // Basic lambda parameter scoping + STATIC_CHECK(evaluate_to(R"( + ((lambda (x) x) 42) + )") == 42); + + // Lambda parameters shadow global scope + STATIC_CHECK(evaluate_to(R"( + (define x 10) + ((lambda (x) x) 42) + )") == 42); + + // Lambda body can access global scope for non-shadowed variables + STATIC_CHECK(evaluate_to(R"( + (define x 10) + ((lambda (y) x) 42) + )") == 10); + + // Lambda parameter shadows global with same name, + // but can still access other globals + STATIC_CHECK(evaluate_to(R"( + (define x 10) + (define y 20) + ((lambda (x) (+ x y)) 30) + )") == 50); +} + +TEST_CASE("Let scoping", "[scoping][let]") +{ + // Basic let binding + STATIC_CHECK(evaluate_to(R"( + (let ((x 10)) x) + )") == 10); + + // Multiple bindings in same let + STATIC_CHECK(evaluate_to(R"( + (let ((x 10) (y 20)) (+ x y)) + )") == 30); + + // Let bindings are not visible outside their scope + STATIC_CHECK(expect_error(R"( + (let ((x 10)) x) + x + )")); + + // Later bindings in the same let can't see earlier ones + STATIC_CHECK(expect_error(R"( + (let ((x 10) (y (+ x 1))) y) + )")); +} + +TEST_CASE("Define scoping", "[scoping][define]") +{ + // Redefining a global is allowed + STATIC_CHECK(evaluate_to(R"( + (define x 10) + (define x 20) + x + )") == 20); + + // Define in nested scopes + STATIC_CHECK(evaluate_to(R"( + (define x 10) + (let ((y 20)) + (define z 30) + (+ x (+ y z))) + )") == 60); + + // Define in a lambda body creates a new binding in that scope + STATIC_CHECK(evaluate_to(R"( + (define counter 0) + (define inc-counter + (lambda () + (define counter (+ counter 1)) + counter)) + (inc-counter) ; this introduces a new counter in the lambda's scope + counter ; global counter remains unchanged + )") == 0); +} + +TEST_CASE("Recursive functions", "[scoping][recursion]") +{ + // Basic recursive function + STATIC_CHECK(evaluate_to(R"( + (define fact + (lambda (n) + (if (== n 0) + 1 + (* n (fact (- n 1)))))) + (fact 5) + )") == 120); + + // Shadowing a recursive function parameter with local binding + STATIC_CHECK(evaluate_to(R"( + (define fact + (lambda (n) + (let ((n 10)) ; This shadows the parameter + n))) + (fact 5) + )") == 10); + + // Ensure recursion still works with shadowed globals + STATIC_CHECK(evaluate_to(R"( + (define x 10) + (define fact + (lambda (n) + (if (== n 0) + 1 + (let ((x (* n (fact (- n 1))))) + x)))) + (fact 5) + )") == 120); +} + +TEST_CASE("Lexical closure capture", "[scoping][closure]") +{ + // Basic closure capturing + STATIC_CHECK(evaluate_to(R"( + (define make-adder + (lambda (x) + (lambda (y) (+ x y)))) + (define add5 (make-adder 5)) + (add5 10) + )") == 15); + + // Nested closures capturing different variables + STATIC_CHECK(evaluate_to(R"( + (define make-adder + (lambda (x) + (lambda (y) + (lambda (z) + (+ x (+ y z)))))) + (define add5 (make-adder 5)) + (define add5and10 (add5 10)) + (add5and10 15) + )") == 30); + + // Captured variables are immutable in the closure (except for self-recursion) + // This system captures values at definition time, not references + STATIC_CHECK(evaluate_to(R"( + (define x 5) + (define get-x (lambda () x)) + (define x 10) + (get-x) + )") == 5); +} + +TEST_CASE("Complex scoping scenarios", "[scoping][complex]") +{ + // Simplified version using just the regular Y-combinator pattern + STATIC_CHECK(evaluate_to(R"( + (define Y + (lambda (f) + ((lambda (x) (f (lambda (y) ((x x) y)))) + (lambda (x) (f (lambda (y) ((x x) y))))))) + + ; A simpler even function using just regular recursion + (define is-even? + (Y (lambda (self) + (lambda (n) + (if (== n 0) + true + (if (== n 1) + false + (self (- n 2)))))))) + + (is-even? 10) + )") == true); + + // Higher-order functions with scoping + STATIC_CHECK(evaluate_to(R"( + (define apply-twice + (lambda (f x) + (f (f x)))) + + (define add5 + (lambda (n) + (+ n 5))) + + (apply-twice add5 10) + )") == 20); + + // IIFE (Immediately Invoked Function Expression) pattern + STATIC_CHECK(evaluate_to(R"( + ((lambda (x) + (define square (lambda (y) (* y y))) + (square x)) + 7) + )") == 49); + + // Demonstrating that attempts to create stateful closures don't work + // because we can't mutate captured variables + STATIC_CHECK(evaluate_to(R"( + (define make-adder + (lambda (x) + (lambda (y) (+ x y)))) + + (define add10 (make-adder 10)) + (add10 5) ; Always returns x+y (10+5) + )") == 15); +} + +TEST_CASE("Edge cases in scoping", "[scoping][edge]") +{ + // Empty body in lambda + STATIC_CHECK(expect_error(R"( + ((lambda (x)) 42) + )")); + + // Empty body in let returns the last expression evaluated (which is nothing) + STATIC_CHECK(evaluate_to(R"( + (let ((x 10))) + )") == std::monostate{}); + + // Self-shadowing in nested let + STATIC_CHECK(evaluate_to(R"( + (let ((x 10)) + (let ((x (+ x 5))) + x)) + )") == 15); + + // Recursive let (not supported in most schemes) + STATIC_CHECK(expect_error(R"( + (let ((fact (lambda (n) + (if (== n 0) + 1 + (* n (fact (- n 1))))))) + (fact 5)) + )")); + + // Named let for recursion (not implemented) + STATIC_CHECK(expect_error(R"( + (let loop ((n 5) (acc 1)) + (if (== n 0) + acc + (loop (- n 1) (* acc n)))) + )")); +} + +TEST_CASE("Y Combinator for anonymous recursion", "[scoping][y-combinator]") +{ + // Using Y-combinator to make an anonymous recursive function + STATIC_CHECK(evaluate_to(R"( + (define Y + (lambda (f) + ((lambda (x) (f (lambda (y) ((x x) y)))) + (lambda (x) (f (lambda (y) ((x x) y))))))) + + ((Y (lambda (fact) + (lambda (n) + (if (== n 0) + 1 + (* n (fact (- n 1))))))) + 5) + )") == 120); + + // Y-combinator with captured variable from outer scope + STATIC_CHECK(evaluate_to(R"( + (define Y + (lambda (f) + ((lambda (x) (f (lambda (y) ((x x) y)))) + (lambda (x) (f (lambda (y) ((x x) y))))))) + + (define multiplier 2) + + ((Y (lambda (fact) + (lambda (n) + (if (== n 0) + 1 + (* multiplier (fact (- n 1))))))) + 5) + )") == 32);// 2^5 +} + +TEST_CASE("Recursive lambda passed to another lambda", "[scoping][recursion][lambda-passing]") +{ + // Define a recursive function, pass it to another function, and verify it still works + STATIC_CHECK(evaluate_to(R"( + ; Define a recursive factorial function + (define factorial + (lambda (n) + (if (== n 0) + 1 + (* n (factorial (- n 1)))))) + + ; Define a function that applies its argument to 5 + (define apply-to-5 + (lambda (f) + (f 5))) + + ; Pass the recursive function to apply-to-5 + (apply-to-5 factorial) + )") == 120); + + // More complex case with a higher-order function that uses the passed function multiple times + STATIC_CHECK(evaluate_to(R"( + ; Define a recursive Fibonacci function + (define fib + (lambda (n) + (if (< n 2) + n + (+ (fib (- n 1)) (fib (- n 2)))))) + + ; Define a function that adds the results of applying a function to two arguments + (define apply-and-add + (lambda (f x y) + (+ (f x) (f y)))) + + ; Pass the recursive function to apply-and-add + (apply-and-add fib 5 6) + )") == 13);// fib(5) + fib(6) = 5 + 8 = 13 + + // A recursive function that returns another recursive function + STATIC_CHECK(evaluate_to(R"( + ; Define a recursive function that returns a specialized power function + (define make-power-fn + (lambda (exponent) + (lambda (base) + (if (== exponent 0) + 1 + (* base ((make-power-fn (- exponent 1)) base)))))) + + ; Get the cube function and apply it to 2 + (define cube (make-power-fn 3)) + (cube 2) + )") == 8);// 2³ = 8 +} diff --git a/test/string_escape_tests.cpp b/test/string_escape_tests.cpp new file mode 100644 index 0000000..7279b99 --- /dev/null +++ b/test/string_escape_tests.cpp @@ -0,0 +1,117 @@ +#include + +#include +#include +#include +#include + +using IntType = int; +using FloatType = double; + +namespace { +template constexpr Result evaluate_to(std::string_view input) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value(); +} + +template constexpr bool evaluate_expected(std::string_view input, auto result) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; +} +}// namespace + +TEST_CASE("String escape processing", "[string][escape]") +{ + // Test basic string with no escapes + STATIC_CHECK(evaluate_expected("\"hello world\"", "hello world")); + + // Test each escape sequence + STATIC_CHECK(evaluate_expected("\"hello\\nworld\"", "hello\nworld")); + STATIC_CHECK(evaluate_expected("\"hello\\tworld\"", "hello\tworld")); + STATIC_CHECK(evaluate_expected("\"hello\\rworld\"", "hello\rworld")); + STATIC_CHECK(evaluate_expected("\"hello\\fworld\"", "hello\fworld")); + STATIC_CHECK(evaluate_expected("\"hello\\bworld\"", "hello\bworld")); + + // Test escaped quotes and backslashes + STATIC_CHECK(evaluate_expected("\"hello\\\"world\"", "hello\"world")); + STATIC_CHECK(evaluate_expected("\"hello\\\\world\"", "hello\\world")); + + // Test multiple escapes in a single string + STATIC_CHECK(evaluate_expected("\"hello\\n\\tworld\\r\"", "hello\n\tworld\r")); + + // Test escapes at start and end + STATIC_CHECK(evaluate_expected("\"\\nhello\"", "\nhello")); + STATIC_CHECK(evaluate_expected("\"hello\\n\"", "hello\n")); + + // Test empty string with escapes + STATIC_CHECK(evaluate_expected("\"\\n\"", "\n")); + STATIC_CHECK(evaluate_expected("\"\\t\\r\\n\"", "\t\r\n")); +} + +TEST_CASE("String escape error cases", "[string][escape][error]") +{ + // Create an evaluator for checking error cases + lefticus::cons_expr evaluator; + + // Test invalid escape sequence + auto invalid_escape = evaluator.evaluate(R"("hello\xworld")"); + REQUIRE(std::holds_alternative>(invalid_escape.value)); + + // Test unterminated escape at end of string + auto unterminated_escape = evaluator.evaluate(R"("hello\")"); + REQUIRE(std::holds_alternative>(unterminated_escape.value)); +} + +TEST_CASE("String operations on escaped strings", "[string][escape][operations]") +{ + // Test comparing strings with escapes + STATIC_CHECK(evaluate_to("(== \"hello\\nworld\" \"hello\\nworld\")") == true); + STATIC_CHECK(evaluate_to("(== \"hello\\nworld\" \"hello\\tworld\")") == false); + + // Test using escaped strings in expressions + STATIC_CHECK(evaluate_expected(R"( + (let ((greeting "Hello\nWorld!")) + greeting) + )", + "Hello\nWorld!")); + + // Test string predicates with escaped strings + STATIC_CHECK(evaluate_to("(string? \"hello\\nworld\")") == true); +} + +TEST_CASE("String escape edge cases", "[string][escape][edge]") +{ + // Test consecutive escapes + STATIC_CHECK(evaluate_expected("\"\\n\\r\\t\"", "\n\r\t")); + + // Test empty string + STATIC_CHECK(evaluate_expected("\"\"", "")); + + // Test string with just an escaped character + STATIC_CHECK(evaluate_expected("\"\\n\"", "\n")); +} + +// Branch Coverage Enhancement Tests - Missing String Cases + +TEST_CASE("String escape error conditions for coverage", "[string][escape][coverage]") +{ + constexpr auto test_unknown_escape = []() constexpr { + lefticus::cons_expr<> engine; + + // Test unknown escape character + auto bad_escape = engine.process_string_escapes("test\\q"); + return std::holds_alternative(bad_escape.value); + }; + STATIC_CHECK(test_unknown_escape()); + + constexpr auto test_unterminated_escape = []() constexpr { + lefticus::cons_expr<> engine; + + // Test unterminated escape (string ends with backslash) + auto unterminated = engine.process_string_escapes("test\\"); + return std::holds_alternative(unterminated.value); + }; + STATIC_CHECK(test_unterminated_escape()); +} diff --git a/test/test_script.scm b/test/test_script.scm new file mode 100644 index 0000000..e175bdd --- /dev/null +++ b/test/test_script.scm @@ -0,0 +1 @@ +(+ 10 20) \ No newline at end of file diff --git a/test/tests.cpp b/test/tests.cpp index c797dd8..9ed9060 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -1,11 +1,15 @@ #include +#include #include #include +#include +#include template using cons_expr_type = lefticus::cons_expr; -void display(cons_expr_type::int_type i) { std::cout << i << '\n'; } +namespace { +void display(cons_expr_type::int_type value) { std::cout << value << '\n'; } auto evaluate(std::basic_string_view input) { @@ -14,9 +18,7 @@ auto evaluate(std::basic_string_view input) evaluator.template add("display"); auto parse_result = evaluator.parse(input); - auto list = std::get::list_type>(parse_result.first.value); - - return evaluator.sequence(evaluator.global_scope, list); + return evaluator.sequence(evaluator.global_scope, parse_result.first); } template Result evaluate_to(std::basic_string_view input) @@ -29,9 +31,7 @@ template auto evaluate_non_char(std::basic_string_vie cons_expr_type evaluator; auto parse_result = evaluator.parse(input); - auto list = std::get::list_type>(parse_result.first.value); - - return evaluator.sequence(evaluator.global_scope, list); + return evaluator.sequence(evaluator.global_scope, parse_result.first); } template @@ -39,6 +39,7 @@ Result evaluate_non_char_to(std::basic_string_view input) { return std::get(std::get::Atom>(evaluate_non_char(input).value)); } +}// namespace TEST_CASE("non-char characters", "[c++ api]") { CHECK(evaluate_non_char_to(L"(+ 1 2 3 4)") == 10); } @@ -54,45 +55,25 @@ TEST_CASE("basic callable usage", "[c++ api]") CHECK(func2(evaluator, 10) == 100); } -TEST_CASE("GPT Generated Tests", "[integration tests]") -{ - CHECK(evaluate_to::int_type, char>(R"( -(define make-adder-multiplier - (lambda (a) - (lambda (b) - (do ((i 0 (+ i 1)) - (sum 0 (+ sum (let ((x (+ a i))) - (if (>= x b) - (define y (* x 2)) - (define y (* x 3))) - (do ((j 0 (+ j 1)) - (inner-sum 0 (+ inner-sum y))) - ((>= j i) inner-sum)))))) - ((>= i 5) sum))))) - -((make-adder-multiplier 2) 3) -)") == 100); -} TEST_CASE("member functions", "[function]") { struct Test { - void set(int i) { m_i = i; } + void set(int new_i) { m_i = new_i; } - int get() const { return m_i; } + [[nodiscard]] int get() const { return m_i; } int m_i{ 0 }; }; - lefticus::cons_expr evaluator; + lefticus::cons_expr evaluator; evaluator.add<&Test::set>("set"); evaluator.add<&Test::get>("get"); auto eval = [&](const std::string_view input) { - return evaluator.sequence( - evaluator.global_scope, std::get(evaluator.parse(input).first.value)); + return evaluator.sequence(evaluator.global_scope, evaluator.parse(input).first); }; Test myobj; @@ -115,6 +96,43 @@ TEST_CASE("basic for-each usage", "[builtins]") CHECK_NOTHROW(evaluate_to("(for-each display '(1 2 3 4))")); } +TEST_CASE("SmallVector error handling", "[core][smallvector]") +{ + constexpr auto test_smallvector_error = []() { + // Create a SmallVector with small capacity + lefticus::SmallVector vec{}; + + // Add elements until we reach capacity + vec.push_back('a'); + vec.push_back('b'); + + // This should set error_state to true + vec.push_back('c'); + + // Check that error_state is set + return vec.error_state == true && vec.size() == static_cast(2); + }; + + STATIC_CHECK(test_smallvector_error()); +} + +TEST_CASE("SmallVector const operator[]", "[core][smallvector]") +{ + constexpr auto test_const_access = []() { + lefticus::SmallVector vec{}; + vec.push_back('a'); + vec.push_back('b'); + vec.push_back('c'); + + // Create a const reference and access elements + const auto &const_vec = vec; + return const_vec[static_cast(0)] == 'a' && const_vec[static_cast(1)] == 'b' + && const_vec[static_cast(2)] == 'c'; + }; + + STATIC_CHECK(test_const_access()); +} + /* struct UDT { @@ -134,4 +152,4 @@ template Result evaluate_to_with_UDT(std::string_view input) return evaluator.eval(context, std::get::List>(parsed.first.value).front()); return std::get(std::get::Atom>(evaluate(input).value)); } - */ \ No newline at end of file + */ diff --git a/test/type_predicate_tests.cpp b/test/type_predicate_tests.cpp new file mode 100644 index 0000000..6edda6b --- /dev/null +++ b/test/type_predicate_tests.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +namespace { +template constexpr Result evaluate_to(std::string_view input) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value(); +} + +template constexpr bool evaluate_expected(std::string_view input, auto result) +{ + lefticus::cons_expr evaluator; + return evaluator.evaluate_to(input).value() == result; +} +}// namespace + +TEST_CASE("Basic type predicates", "[types][predicates]") +{ + // integer? + STATIC_CHECK(evaluate_to("(integer? 42)") == true); + STATIC_CHECK(evaluate_to("(integer? 3.14)") == false); + STATIC_CHECK(evaluate_to("(integer? \"hello\")") == false); + STATIC_CHECK(evaluate_to("(integer? '(1 2 3))") == false); + + // real? + STATIC_CHECK(evaluate_to("(real? 3.14)") == true); + STATIC_CHECK(evaluate_to("(real? 42)") == false); + STATIC_CHECK(evaluate_to("(real? \"hello\")") == false); + + // string? + STATIC_CHECK(evaluate_to("(string? \"hello\")") == true); + STATIC_CHECK(evaluate_to("(string? 42)") == false); + STATIC_CHECK(evaluate_to("(string? 3.14)") == false); + + // boolean? + STATIC_CHECK(evaluate_to("(boolean? true)") == true); + STATIC_CHECK(evaluate_to("(boolean? false)") == true); + STATIC_CHECK(evaluate_to("(boolean? 42)") == false); + STATIC_CHECK(evaluate_to("(boolean? \"true\")") == false); + + // symbol? + STATIC_CHECK(evaluate_to("(symbol? 'abc)") == true); + STATIC_CHECK(evaluate_to("(symbol? \"abc\")") == false); + STATIC_CHECK(evaluate_to("(symbol? 42)") == false); +} + +TEST_CASE("Composite type predicates", "[types][predicates]") +{ + // number? + STATIC_CHECK(evaluate_to("(number? 42)") == true); + STATIC_CHECK(evaluate_to("(number? 3.14)") == true); + STATIC_CHECK(evaluate_to("(number? \"42\")") == false); + STATIC_CHECK(evaluate_to("(number? '(1 2 3))") == false); + + // list? + STATIC_CHECK(evaluate_to("(list? '())") == true); + STATIC_CHECK(evaluate_to("(list? '(1 2 3))") == true); + STATIC_CHECK(evaluate_to("(list? (list 1 2 3))") == true); + STATIC_CHECK(evaluate_to("(list? 42)") == false); + STATIC_CHECK(evaluate_to("(list? \"hello\")") == false); + + // procedure? + STATIC_CHECK(evaluate_to("(procedure? (lambda (x) x))") == true); + STATIC_CHECK(evaluate_to("(procedure? +)") == true); + STATIC_CHECK(evaluate_to("(procedure? 42)") == false); + STATIC_CHECK(evaluate_to("(procedure? '(1 2 3))") == false); + + // atom? + STATIC_CHECK(evaluate_to("(atom? 42)") == true); + STATIC_CHECK(evaluate_to("(atom? \"hello\")") == true); + STATIC_CHECK(evaluate_to("(atom? true)") == true); + STATIC_CHECK(evaluate_to("(atom? 'abc)") == true); + STATIC_CHECK(evaluate_to("(atom? '(1 2 3))") == false); + STATIC_CHECK(evaluate_to("(atom? (lambda (x) x))") == false); +} + +TEST_CASE("Type predicates in expressions", "[types][predicates]") +{ + // Using predicates in if expressions + STATIC_CHECK(evaluate_to(R"( + (if (number? 42) + 1 + 0) + )") == 1); + + STATIC_CHECK(evaluate_to(R"( + (if (string? 42) + 1 + 0) + )") == 0); + + // Using predicates in lambda functions + STATIC_CHECK(evaluate_to(R"( + (define type-checker + (lambda (x) + (cond + ((number? x) true) + ((string? x) true) + (else false)))) + + (type-checker 42) + )") == true); + + STATIC_CHECK(evaluate_to(R"( + (define type-checker + (lambda (x) + (cond + ((number? x) true) + ((string? x) true) + (else false)))) + + (type-checker '(1 2 3)) + )") == false); +} diff --git a/web/coi-serviceworker.min.js b/web/coi-serviceworker.min.js new file mode 100644 index 0000000..33fddc0 --- /dev/null +++ b/web/coi-serviceworker.min.js @@ -0,0 +1,2 @@ +/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ +let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); diff --git a/web/index_template.html.in b/web/index_template.html.in new file mode 100644 index 0000000..1418c85 --- /dev/null +++ b/web/index_template.html.in @@ -0,0 +1,77 @@ + + + + + + @PROJECT_NAME@ - WebAssembly Applications + + + + +
+
+ + + +
+ +
+

@PROJECT_NAME@

+

WebAssembly Applications

+
+ +
+ @WASM_APPS_HTML@ +
+ +
+ Built with CMake and Emscripten | Version @PROJECT_VERSION@ +
+
+ + + + diff --git a/web/shell_template_console.html.in b/web/shell_template_console.html.in new file mode 100644 index 0000000..a2ddb58 --- /dev/null +++ b/web/shell_template_console.html.in @@ -0,0 +1,317 @@ + + + + + + @TARGET_TITLE@ + + + + + + + + +
+
+ + + +
+ +
+

@TARGET_NAME@

+
+ +
+
+

Loading @TARGET_NAME@...

+

+
+ + + +
+ +
+

Command-Line Arguments via URL

+

Pass command-line arguments to the application using URL parameters:

+ + Conversion Rules: +
    +
  • • Single-character params → short flags: ?v-v
  • +
  • • Multi-character params → long flags: ?version--version
  • +
  • • Params with values: ?file=test.txt--file test.txt
  • +
  • • Multiple params: ?v&help-v --help
  • +
+ + Examples: +
    +
  • intro.html?version
  • +
  • intro.html?h
  • +
  • intro.html?file=data.txt
  • +
  • intro.html?verbose&config=settings.json
  • +
+
+
+ + + + + + + + + + + {{{ SCRIPT }}} + + diff --git a/web/shell_template_ftxui.html.in b/web/shell_template_ftxui.html.in new file mode 100644 index 0000000..6596cae --- /dev/null +++ b/web/shell_template_ftxui.html.in @@ -0,0 +1,330 @@ + + + + + + @TARGET_TITLE@ + + + + + + + + +
+
+ + + +
+ +
+

@TARGET_NAME@

+
+ +
+
+

Loading @TARGET_NAME@...

+

+
+ + + +
+ +
+

Command-Line Arguments via URL

+

Pass command-line arguments to the application using URL parameters:

+ + Conversion Rules: +
    +
  • • Single-character params → short flags: ?v-v
  • +
  • • Multi-character params → long flags: ?version--version
  • +
  • • Params with values: ?file=test.txt--file test.txt
  • +
  • • Multiple params: ?v&help-v --help
  • +
+ + Examples: +
    +
  • intro.html?version
  • +
  • intro.html?h
  • +
  • intro.html?file=data.txt
  • +
  • intro.html?verbose&config=settings.json
  • +
+
+
+ + + + + + + + + + + {{{ SCRIPT }}} + +