diff --git a/.clang-tidy b/.clang-tidy index 219fc86..b4f009d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -18,11 +18,19 @@ WarningsAsErrors: '' HeaderFilterRegex: '' FormatStyle: none +# 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' readability-identifier-length.IgnoredParameterNames: 'x|y|z|id|ch' - - - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef78340..5a95eb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - ubuntu-22.04 compiler: # you can specify the version after `-` like "llvm-16.0.0". - - gcc-13 + - gcc-14 generator: - "Ninja Multi-Config" build_type: @@ -45,7 +45,7 @@ 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 + - compiler: gcc-14 gcov_executable: gcov enable_ipo: On 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/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/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/cmake/StaticAnalyzers.cmake b/cmake/StaticAnalyzers.cmake index a853482..d0bb71e 100644 --- a/cmake/StaticAnalyzers.cmake +++ b/cmake/StaticAnalyzers.cmake @@ -74,6 +74,9 @@ 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 + --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..4513bbd 100644 --- a/examples/compile_test.cpp +++ b/examples/compile_test.cpp @@ -24,8 +24,7 @@ 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); } diff --git a/examples/speed_test.cpp b/examples/speed_test.cpp index 5caa062..de5c439 100644 --- a/examples/speed_test.cpp +++ b/examples/speed_test.cpp @@ -14,13 +14,12 @@ 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)); } int main() diff --git a/fuzz_test/CMakeLists.txt b/fuzz_test/CMakeLists.txt index 60e096b..459cd48 100644 --- a/fuzz_test/CMakeLists.txt +++ b/fuzz_test/CMakeLists.txt @@ -8,6 +8,7 @@ target_link_libraries( fuzz_tester PRIVATE cons_expr_options cons_expr_warnings + cons_expr fmt::fmt -coverage -fsanitize=fuzzer) diff --git a/fuzz_test/fuzz_tester.cpp b/fuzz_test/fuzz_tester.cpp index ed0fc4c..14d4ffe 100644 --- a/fuzz_test/fuzz_tester.cpp +++ b/fuzz_test/fuzz_tester.cpp @@ -1,22 +1,24 @@ +#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; + // Create a string view from the fuzz data + std::string_view script(reinterpret_cast(data), 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 +} \ No newline at end of file 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/main.cpp b/src/ccons_expr/main.cpp index 8bd30d4..1af0107 100644 --- a/src/ccons_expr/main.cpp +++ b/src/ccons_expr/main.cpp @@ -50,9 +50,8 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char *argv[]) } 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[{ 0, evaluator.global_scope.size() }]) { globals.push_back(std::format("{}: '{}'", to_string(evaluator, false, key), to_string(evaluator, true, value))); - ++index; } }; @@ -66,10 +65,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(); } diff --git a/src/cons_expr_cli/main.cpp b/src/cons_expr_cli/main.cpp index a7874e7..c92dd08 100644 --- a/src/cons_expr_cli/main.cpp +++ b/src/cons_expr_cli/main.cpp @@ -1,7 +1,10 @@ #include +#include #include +#include #include +#include #include #include @@ -9,9 +12,22 @@ #include using cons_expr_type = lefticus::cons_expr<>; +namespace fs = std::filesystem; 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 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(); +} int main(int argc, const char **argv) { @@ -19,9 +35,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 +52,37 @@ 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 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) { std::cout << app.help() << '\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..0565b8f --- /dev/null +++ b/test/cond_tests.cpp @@ -0,0 +1,164 @@ +#include +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +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); +} + +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..68a3519 100644 --- a/test/constexpr_tests.cpp +++ b/test/constexpr_tests.cpp @@ -29,15 +29,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); @@ -46,6 +42,18 @@ template constexpr std::optional parse_as(auto &evaluat return *result; } +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]") { STATIC_CHECK(evaluate_to("((if false + *) 3 4)") == 12); @@ -65,6 +73,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 +96,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 +157,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 +220,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 +334,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 +537,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 +564,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 +609,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 +666,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); @@ -823,8 +788,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 id{ 5, 10 }; + return id.start == 5 && id.size == 10; }; STATIC_CHECK(test_identifier_creation()); } @@ -832,8 +797,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 id1{ 5, 10 }; + lefticus::Identifier id2{ 5, 10 }; return id1 == id2; }; STATIC_CHECK(test_identifier_equality()); @@ -842,8 +807,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 +817,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 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()); } @@ -1013,8 +978,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 +1021,1149 @@ 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[j] = static_cast('a' + (static_cast(i) + j) % 26); + } + std::string_view 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 (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)"); + 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))"); + 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("(== \"hello\" \"hello\")"); + 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))"); + 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)"); + 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)"); + 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))"); + 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))"); + 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))"); + 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)"); + 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 + 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 + 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 + 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) + 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)"); + auto *bool_ptr1 = engine.get_if(&result1); + if (!bool_ptr1 || !*bool_ptr1) return false; + + // Test string? predicate + auto result2 = engine.evaluate("(string? \"hello\")"); + auto *bool_ptr2 = engine.get_if(&result2); + if (!bool_ptr2 || !*bool_ptr2) return false; + + // Test boolean? predicate + auto result3 = engine.evaluate("(boolean? true)"); + 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 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))"); + 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))"); + 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))"); + 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) '())"); + 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))"); + 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))"); + 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)"); + 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\")"); + 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, _] = engine.parse(" \t 42 \t "); + if (parsed1.size != 1) return false; + + // Test parsing with mixed whitespace + auto [parsed2, __] = 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("\"\\n\\t\\r\\f\\b\\\"\\\\\""); + auto *str1 = engine.get_if::string_type>(&result1); + if (str1 == nullptr) return false; + + // Test string with mixed content + auto result2 = engine.evaluate("\"Hello\\nWorld\""); + 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 + auto *list1 = engine.get_if::list_type>(&result1); + 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)"); + 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)"); + 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, _] = engine.parse("-"); + if (parsed1.size != 1) return false; + auto result1 = engine.values[parsed1[0]]; + // Should be parsed as identifier, not number + auto *id1 = engine.get_if::identifier_type>(&result1); + if (id1 == nullptr) return false; + + // Test malformed numbers + auto [parsed2, __] = 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 + 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 + auto *error2 = engine.get_if::error_type>(&result2); + 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 + 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 '())"); + 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)"); + 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("\"\\x\"");// Invalid escape + // Should either parse successfully (treating as literal) or error + auto *str1 = engine.get_if::string_type>(&result1); + 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 + auto *literal_list1 = engine.get_if::literal_list_type>(&result1); + 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, _] = engine.parse("42 ; comment"); + if (parsed1.size != 1) return false; + + // Test multiple consecutive comments + auto [parsed2, __] = 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 + 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\")"); + 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)"); + auto *float1 = engine.get_if(&result1); + if (float1 == nullptr) return false; + + // Test floating point comparison precision + auto result2 = engine.evaluate("(== 0.1 0.1)"); + 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))))"); + 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))"); + 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))"); + 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 + auto *int2 = engine.get_if(&result2); + 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 + 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))"); + 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]") +{ + // Test parse_number edge cases for lines 263, 283, 288, 296, 310, 319, 334, 343, 351 + constexpr auto test_number_parsing = []() constexpr { + // Test lone minus (line 263) + auto r1 = lefticus::parse_number("-"); + if (r1.first) return false; + + // Test scientific notation with 'e' and 'E' (lines 310, 319) + auto r2 = lefticus::parse_number("1.5e10"); + if (!r2.first) return false; + + auto r3 = lefticus::parse_number("1.5E10"); + if (!r3.first) return false; + + // Test negative exponent (line 343) + auto r4 = lefticus::parse_number("1.5e-2"); + if (!r4.first || r4.second != 0.015) return false; + + // Test invalid exponent character (line 334) + auto r5 = lefticus::parse_number("1.5ex"); + if (r5.first) return false; + + // Test incomplete exponent (line 351) + auto r6 = lefticus::parse_number("1.5e"); + if (r6.first) return false; + + // Test decimal starting with dot (line 301 in original) + auto r7 = lefticus::parse_number(".5"); + if (!r7.first || r7.second != 0.5) return false; + + return true; + }; + STATIC_CHECK(test_number_parsing()); +} + +TEST_CASE("Branch coverage - Token parsing edge cases", "[coverage][parser]") +{ + // Test next_token edge cases (lines 372, 389, 392, 410, 415, 417) + constexpr auto test_token_parsing = []() constexpr { + // Test CR/LF handling (line 372) + auto t1 = lefticus::next_token("\r\ntest"); + if (t1.parsed != "test") return false; + + auto t2 = lefticus::next_token("\r test"); + if (t2.parsed != "test") return false; + + // Test quote character (line 392) + auto t3 = lefticus::next_token("'symbol"); + if (t3.parsed != "'" || t3.remaining != "symbol") return false; + + // Test parentheses (lines 389, 410) + auto t4 = lefticus::next_token("(test)"); + if (t4.parsed != "(" || t4.remaining != "test)") return false; + + auto t5 = lefticus::next_token(")rest"); + if (t5.parsed != ")" || t5.remaining != "rest") return false; + + // Test unterminated string (line 415) + auto t6 = lefticus::next_token("\"unterminated"); + if (t6.parsed != "\"unterminated" || !t6.remaining.empty()) return false; + + // Test empty input (line 417) + auto t7 = lefticus::next_token(""); + return t7.parsed.empty() && t7.remaining.empty(); + }; + STATIC_CHECK(test_token_parsing()); +} + +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 r1 = engine.process_string_escapes("test\\"); + auto *e1 = engine.get_if::error_type>(&r1); + if (e1 == nullptr) return false; + + // Test unknown escape char (line 538) + auto r2 = engine.process_string_escapes("test\\q"); + auto *e2 = engine.get_if::error_type>(&r2); + if (e2 == nullptr) return false; + + // Test valid escapes + auto r3 = engine.process_string_escapes("\\n\\t\\r\\\"\\\\"); + auto *s3 = engine.get_if::string_type>(&r3); + return s3 != 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 msg1{ 0, 10 }; + lefticus::IndexedString msg2{ 0, 10 }; + lefticus::IndexedList list1{ 0, 5 }; + lefticus::IndexedList list2{ 0, 5 }; + + Error e1{ msg1, list1 }; + Error e2{ msg2, list2 }; + Error e3{ msg1, lefticus::IndexedList{ 1, 5 } }; + + return e1 == e2 && !(e1 == e3); + }; + 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 r1 = engine.evaluate("(lambda (x) (+ x 1))"); + auto *closure1 = engine.get_if::Closure>(&r1); + if (closure1 == nullptr) return false; + + // Test let identifier fixing + auto r2 = engine.evaluate("(let ((x 5)) x)"); + auto *int2 = engine.get_if(&r2); + if (int2 == nullptr || *int2 != 5) return false; + + // Test define identifier fixing + auto r3 = engine.evaluate("(define foo 42) foo"); + auto *int3 = engine.get_if(&r3); + 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 r1 = engine.evaluate("(define f (lambda (x) x))"); + [[maybe_unused]] auto r2 = engine.evaluate("(define g (lambda (x) x))"); + auto result = engine.evaluate("(== f g)"); + 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 r1 = engine.evaluate("(define f (lambda (x) x))"); + auto result = engine.evaluate("(== f f)"); + 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..fa1a690 --- /dev/null +++ b/test/error_handling_tests.cpp @@ -0,0 +1,103 @@ +#include +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +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); +} + +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..9eac538 --- /dev/null +++ b/test/list_construction_tests.cpp @@ -0,0 +1,145 @@ +#include +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +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; +} + +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..9593c32 100644 --- a/test/list_tests.cpp +++ b/test/list_tests.cpp @@ -225,15 +225,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..42f5edf 100644 --- a/test/parser_tests.cpp +++ b/test/parser_tests.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -6,39 +7,11 @@ #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{}; -} - -// Helper function for checking if a parsed expression contains a specific type -template constexpr bool is_of_type(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) { - return evaluator.template get_if(&evaluator.values[(*list)[0]]) != nullptr; - } - - return false; -} // Basic Tokenization Tests TEST_CASE("Basic tokenization", "[parser][tokenize]") @@ -71,7 +44,7 @@ TEST_CASE("Basic tokenization", "[parser][tokenize]") // 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; + if (token7.parsed != std::string_view("'") || token7.remaining != std::string_view("(hello)")) return false; // Strings Token token8 = lefticus::next_token(std::string_view("\"hello\"")); @@ -286,6 +259,94 @@ TEST_CASE("String parsing", "[parser][strings]") STATIC_CHECK(test_string5()); } +using evaluator_type = lefticus::cons_expr; + +template constexpr auto parse(std::basic_string_view str) +{ + evaluator_type evaluator; + const auto list = evaluator.parse(str).first; + + if (list.size != 1) throw "expected exactly one thing parsed"; + + return evaluator.values[list[0]]; +} + +TEST_CASE("String parse failures", "[parser][strings][escapes]") +{ + STATIC_CHECK(std::holds_alternative(parse(std::string_view(R"("\q")")).value)); + STATIC_CHECK(std::holds_alternative(parse(std::string_view(R"("\q)")).value)); +} + +// 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("\"Quote: \\\"Hello\\\"\""); + + // 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("\"Quote: \\\"Hello\\\"\"")); + if (token.parsed != std::string_view("\"Quote: \\\"Hello\\\"\"")) return false; + + return true; + }; + + // Escaped backslash + constexpr auto test_escaped_backslash = []() { + auto token = lefticus::next_token(std::string_view("\"Backslash: \\\\\"")); + return token.parsed == std::string_view("\"Backslash: \\\\\""); + }; + + // Multiple escape sequences + constexpr auto test_multiple_escapes = []() { + auto token = lefticus::next_token(std::string_view("\"Escapes: \\\\ \\\" \\n \\t \\r\"")); + return token.parsed == std::string_view("\"Escapes: \\\\ \\\" \\n \\t \\r\""); + }; + + // Escape at end of string + constexpr auto test_escape_at_end = []() { + auto token = lefticus::next_token(std::string_view("\"Escape at end: \\\"")); + return token.parsed == std::string_view("\"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("\"Special chars: \\n\\t\\r\\f\\b\"")); + return token.parsed == std::string_view("\"Special chars: \\n\\t\\r\\f\\b\""); + }; + + // Test handling of consecutive escapes + constexpr auto test_consecutive_escapes = []() { + auto token = lefticus::next_token(std::string_view("\"Double escapes: \\\\\\\"\"")); + return token.parsed == std::string_view("\"Double escapes: \\\\\\\"\""); + }; + + // 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()); +} + // Number Parsing Tests TEST_CASE("Number parsing", "[parser][numbers]") { @@ -303,28 +364,7 @@ TEST_CASE("Number parsing", "[parser][numbers]") 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; - - auto [success2, value2] = lefticus::parse_number(std::string_view("-789.012")); - if (!success2 || std::abs(value2 - (-789.012)) > 0.0001) return false; - - auto [success3, value3] = lefticus::parse_number(std::string_view("1e3")); - if (!success3 || std::abs(value3 - 1000.0) > 0.0001) return false; - - auto [success4, value4] = lefticus::parse_number(std::string_view("1.5e-2")); - if (!success4 || std::abs(value4 - 0.015) > 0.0001) return false; - - auto [success5, value5] = lefticus::parse_number(std::string_view("not_a_number")); - if (success5) return false;// Should fail - - return true; - }; - STATIC_CHECK(test_int_parsing()); - STATIC_CHECK(test_float_parsing()); } // List Structure Tests @@ -335,15 +375,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 +392,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,14 +408,13 @@ 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; @@ -393,70 +430,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,10 +439,9 @@ 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); + 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); @@ -477,10 +449,9 @@ TEST_CASE("Boolean literals", "[parser][booleans]") // 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); + 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); @@ -502,11 +473,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,11 +495,10 @@ 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; @@ -552,10 +521,9 @@ 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); + 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); @@ -580,11 +548,10 @@ 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; @@ -600,45 +567,6 @@ 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]") { @@ -663,4 +591,81 @@ TEST_CASE("Special characters", "[parser][special-chars]") }; 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<> engine; + + // Test null pointer in get_if + const decltype(engine)::SExpr *null_ptr = nullptr; + 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..40fa901 --- /dev/null +++ b/test/recursion_and_closure_tests.cpp @@ -0,0 +1,96 @@ +#include +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +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; +} + +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/scoping_tests.cpp b/test/scoping_tests.cpp new file mode 100644 index 0000000..9499530 --- /dev/null +++ b/test/scoping_tests.cpp @@ -0,0 +1,396 @@ +#include +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +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); +} + +// ----- 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 +} \ No newline at end of file diff --git a/test/string_escape_tests.cpp b/test/string_escape_tests.cpp new file mode 100644 index 0000000..d3d325a --- /dev/null +++ b/test/string_escape_tests.cpp @@ -0,0 +1,115 @@ +#include +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +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; +} + +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("\"hello\\xworld\""); + REQUIRE(std::holds_alternative>(invalid_escape.value)); + + // Test unterminated escape at end of string + auto unterminated_escape = evaluator.evaluate("\"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..38f8e0b 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -14,9 +14,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 +27,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 @@ -54,25 +50,6 @@ 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]") { @@ -85,14 +62,13 @@ TEST_CASE("member functions", "[function]") 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 +91,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 +147,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..8712127 --- /dev/null +++ b/test/type_predicate_tests.cpp @@ -0,0 +1,120 @@ +#include +#include + +#include +#include +#include + +using IntType = int; +using FloatType = double; + +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; +} + +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); +}