Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands
- Configure: `cmake -S . -B ./build`
- Build: `cmake --build ./build`
- Run tests: `cd ./build && ctest -C Debug`
- Run specific test: `cd ./build && ctest -C Debug -R "unittests.*"` or `ctest -C Debug -R "constexpr.*"`
- Build constexpr tests: `cmake --build ./build --target constexpr_tests`
- Build runtime assertion tests: `cmake --build ./build --target relaxed_constexpr_tests`
- Run a specific test category: `./test/relaxed_constexpr_tests "[category]"`

## Testing
- The library is designed to run both at compile-time and runtime
- `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
- `relaxed_constexpr_tests` target compiles with runtime assertions
- Preferred for debugging since it shows which specific tests fail
- Use this target when developing/debugging:
```bash
cmake --build ./build --target relaxed_constexpr_tests && ./build/test/relaxed_constexpr_tests
```

### Catch2 Command Line Arguments
- Run specific test tag: `./build/test/relaxed_constexpr_tests "[tag]"`
- Run tests with specific name: `./build/test/relaxed_constexpr_tests "quote function"`
- Increase verbosity: `./build/test/relaxed_constexpr_tests --verbosity high`
- List all tests: `./build/test/relaxed_constexpr_tests --list-tests`
- List all tags: `./build/test/relaxed_constexpr_tests --list-tags`
- Show help: `./build/test/relaxed_constexpr_tests --help`

### Command-line Debugging
- Prefer using the `cons_expr` command-line tool for quick debugging and iteration
- Build it with: `cmake --build ./build --target cons_expr_cli`
- Test expressions directly with: `./build/src/cons_expr_cli/cons_expr_cli --exec "(expression-to-test)"`
- This is faster than rebuilding and running test suites for quick iteration

### Writing Tests
- All tests should pass in both modes (constexpr and runtime)
- Catch2 is used for testing framework
- Use the TEST_CASE macro with meaningful name and tags
- Split complex tests into smaller, focused tests
- All constexpr tests should use STATIC_CHECK to ensure they can be evaluated at compile time
- When testing parsing functions:
- Use `std::string_view` when passing string literals
- Remember `parse()` always returns a list containing the parsed expressions
- Navigate the result carefully by checking the types at each level
- Separate parse tests (parser_tests.cpp) from evaluation tests (constexpr_tests.cpp)

## Code Style
- C++23 standard
- No C++ extensions (CMAKE_CXX_EXTENSIONS OFF)
- Treat warnings as errors
- Code is header-only library (include/cons_expr)
- 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

## Naming and Structure
- Namespace: lefticus
- Use snake_case for variables and functions
- Classes/structs use PascalCase
- Template parameters use PascalCase
- All objects are immutable once captured

## Error Handling
- Avoid exceptions and dynamic allocations
- Use std::expected for error handling
- Check bounds and sizes before access

## Parser and Expression Structure
- `parse()` function always returns a list containing the parsed expressions
- Even a single expression is wrapped in a list
- Expression types are stored as variants
- Elements can be:
- Atoms (identifiers, symbols, strings, numbers, booleans)
- Lists (collections of elements)
- Literal lists (quoted lists that aren't evaluated)
- Closures (lambda functions with environment)
- Errors (with expected and got information)
- The parser handles nested structures, quotes, and comments
- Use string_view for all string literals in parser tests

## Known Issues
- String handling: Special attention needed for escaped quotes in strings
86 changes: 74 additions & 12 deletions include/cons_expr/cons_expr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,15 @@ template<std::unsigned_integral SizeType> struct Identifier
using size_type = SizeType;
IndexedString<size_type> value;
[[nodiscard]] constexpr auto substr(const size_type from) const { return Identifier{ value.substr(from) }; }
[[nodiscard]] constexpr bool operator==(const Identifier &) const noexcept = default;
[[nodiscard]] constexpr bool operator==(const Identifier &other) const noexcept = default;
};

template<std::unsigned_integral SizeType> struct Symbol
{
using size_type = SizeType;
IndexedString<size_type> 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<std::unsigned_integral SizeType> Identifier(IndexedString<SizeType>) -> Identifier<SizeType>;
Expand Down Expand Up @@ -490,6 +498,7 @@ struct cons_expr
using string_type = IndexedString<size_type>;
using string_view_type = std::basic_string_view<char_type>;
using identifier_type = Identifier<size_type>;
using symbol_type = Symbol<size_type>;
using list_type = IndexedList<size_type>;
using literal_list_type = LiteralList<size_type>;
using error_type = Error<size_type>;
Expand Down Expand Up @@ -525,7 +534,8 @@ struct cons_expr

using LexicalScope = SmallVector<size_type, std::pair<string_type, SExpr>, BuiltInSymbolsSize, list_type>;
using function_ptr = SExpr (*)(cons_expr &, LexicalScope &, list_type);
using Atom = std::variant<std::monostate, bool, int_type, float_type, string_type, identifier_type, UserTypes...>;
using Atom =
std::variant<std::monostate, bool, int_type, float_type, string_type, identifier_type, symbol_type, UserTypes...>;

struct FunctionPtr
{
Expand Down Expand Up @@ -561,7 +571,7 @@ struct cons_expr
static_assert(std::is_trivially_copyable_v<SExpr> && std::is_trivially_destructible_v<SExpr>,
"cons_expr does not work with non-trivial types");

template<typename Result> [[nodiscard]] constexpr const Result *get_if(const SExpr *sexpr) const noexcept
template<typename Result> [[nodiscard]] static constexpr const Result *get_if(const SExpr *sexpr) noexcept
{
if (sexpr == nullptr) { return nullptr; }

Expand Down Expand Up @@ -695,6 +705,8 @@ struct cons_expr
retval.push_back(SExpr{ Atom(int_value) });
} else if (auto [float_did_parse, float_value] = parse_number<float_type>(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)) }) });
} else {
retval.push_back(SExpr{ Atom(Identifier{ strings.insert_or_find(token.parsed) }) });
}
Expand Down Expand Up @@ -733,6 +745,7 @@ struct cons_expr
add(str("append"), SExpr{ FunctionPtr{ append, FunctionPtr::Type::other } });
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 } });
}

[[nodiscard]] constexpr SExpr sequence(LexicalScope &scope, list_type expressions)
Expand Down Expand Up @@ -841,11 +854,6 @@ struct cons_expr
if (key == id->value) { return value; }
}

const auto string = strings.view(id->value);

// is quoted identifier, handle appropriately
if (string.starts_with('\'')) { return SExpr{ Atom{ id->substr(1) } }; }

return make_error(str("id not found"), expr);
}
return expr;
Expand Down Expand Up @@ -1271,6 +1279,14 @@ struct cons_expr

if (const auto *list_front = std::get_if<literal_list_type>(&front.value); list_front != nullptr) {
result.push_back(SExpr{ list_front->items });
} else if (const auto *atom = std::get_if<Atom>(&front.value); atom != nullptr) {
if (const auto *identifier_front = std::get_if<symbol_type>(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 } } });
} else {
result.push_back(front);
}
} else {
result.push_back(front);
}
Expand All @@ -1292,14 +1308,34 @@ struct cons_expr

[[nodiscard]] static constexpr SExpr cdr(cons_expr &engine, LexicalScope &scope, list_type params)
{
return error_or_else(engine.eval_to<literal_list_type>(scope, params, str("(cdr Non-Empty-LiteralList)")),
[&](const auto &list) { return SExpr{ list.sublist(1) }; });
return error_or_else(
engine.eval_to<literal_list_type>(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<size_type> empty_list{ 0, 0 };
return SExpr{ literal_list_type{ empty_list } };
}
return SExpr{ list.sublist(1) };
});
}

[[nodiscard]] static constexpr SExpr car(cons_expr &engine, LexicalScope &scope, list_type params)
{
return error_or_else(engine.eval_to<literal_list_type>(scope, params, str("(car Non-Empty-LiteralList)")),
[&](const auto &list) { return engine.values[list.front()]; });
return error_or_else(
engine.eval_to<literal_list_type>(scope, params, str("(car Non-Empty-LiteralList)")), [&](const auto &list) {
// Check if list is empty
if (list.items.size == 0) { return engine.make_error(str("car: cannot take car of empty list"), params); }

// Get the first element of the list
const auto &elem = engine.values[list.items.front()];

// If the element is a list_type, return it as a literal_list_type
if (const auto *nested_list = std::get_if<list_type>(&elem.value); nested_list != nullptr) {
return SExpr{ literal_list_type{ *nested_list } };
}

return elem;
});
}

[[nodiscard]] static constexpr SExpr applier(cons_expr &engine, LexicalScope &scope, list_type params)
Expand Down Expand Up @@ -1345,6 +1381,32 @@ struct cons_expr
return SExpr{ Atom{ std::monostate{} } };
}

[[nodiscard]] static constexpr SExpr quoter(cons_expr &engine, LexicalScope &, list_type params)
{
if (params.size != 1) { return engine.make_error(str("(quote expr)"), params); }

const auto &expr = engine.values[params[0]];

// If it's a list, convert it to a literal list
if (const auto *list = std::get_if<list_type>(&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<size_type> empty_list{ 0, 0 };
return SExpr{ literal_list_type{ empty_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<Atom>(&expr.value); atom != nullptr) {
if (const auto *id = std::get_if<identifier_type>(atom); id != nullptr) {
return SExpr{ Atom{ symbol_type{ id->value } } };
}
}

// Otherwise return as is
return expr;
}

[[nodiscard]] static constexpr SExpr definer(cons_expr &engine, LexicalScope &scope, list_type params)
{
return error_or_else(engine.eval_to<identifier_type, SExpr>(scope, params, str("(define Identifier Expression)")),
Expand Down
4 changes: 2 additions & 2 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ catch_discover_tests(
.xml)

# Add a file containing a set of constexpr tests
add_executable(constexpr_tests constexpr_tests.cpp)
add_executable(constexpr_tests constexpr_tests.cpp list_tests.cpp parser_tests.cpp)
target_link_libraries(
constexpr_tests
PRIVATE cons_expr::cons_expr
Expand Down Expand Up @@ -93,7 +93,7 @@ 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)
add_executable(relaxed_constexpr_tests constexpr_tests.cpp list_tests.cpp parser_tests.cpp)
target_link_libraries(
relaxed_constexpr_tests
PRIVATE cons_expr::cons_expr
Expand Down
Loading
Loading