Skip to content

Replace exceptions with expected - add tests #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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 -G Ninja` or `cmake . --preset <configure-preset>` (CMake 3.21+)
- Build: `cmake --build ./build`
- Run tests: `cd ./build && ctest -C Debug && cd ..`
- Run specific test: `cd ./build && ./test/tests "[test name]" && cd ..`
- Static analysis: Enable with `-Dinfiz_ENABLE_CLANG_TIDY=ON` or `-Dinfiz_ENABLE_CPPCHECK=ON`

## Code Style Guidelines
- Standard: C++23
- Formatting: 2-space indentation, 120 char line limit, braces on new lines for functions/classes
- Naming: Classes=PascalCase, Methods/Variables=camelCase, Constants/Enums=UPPER_CASE
- Error handling: Use exceptions with descriptive messages, lambda error handlers
- Features: Use `constexpr`, `consteval`, `[[nodiscard]]`, concepts, format strings
- Headers: Include guards with `INFIZ_NAME_H` format, sort includes logically
- Functions: Prefer pure functions marked with `[[nodiscard]]` where possible
34 changes: 30 additions & 4 deletions src/infiz/infiz.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ auto main() -> int
std::cin.getline(input.data(), max_line - 1, '\n');

while (std::cin.good()) {
try {
const auto answer = evaluate(input.data());
auto result = evaluate(input.data());

if (result) {
const auto answer = *result;
std::cout << "answer: ";

if (answer.getDenominator() == 1) {
Expand All @@ -25,8 +27,32 @@ auto main() -> int
std::cout << std::format(
"{}/{} ({})\n", answer.getNumerator(), answer.getDenominator(), answer.asFloat<double>());
}
} catch (const std::runtime_error &err) {
std::cout << err.what() << '\n';
} else {
// Format the error message with the expression and position indicator
const auto &error = result.error();

// Get error type message
std::string errorTypeMsg;
switch (error.type) {
case EvaluationError::ErrorType::INVALID_NUMBER:
errorTypeMsg = "Invalid number - expected only digits";
break;
case EvaluationError::ErrorType::EMPTY_TOKEN:
errorTypeMsg = "Empty token encountered";
break;
case EvaluationError::ErrorType::STACK_ERROR:
errorTypeMsg = "Stack error - expression might be malformed";
break;
case EvaluationError::ErrorType::INVALID_EXPRESSION:
default:
errorTypeMsg = "Invalid expression";
break;
}

// Print a visually appealing error with position indicator
std::cout << "\033[1;31mError:\033[0m " << errorTypeMsg << "\n\n";
std::cout << " " << input.data() << "\n";
std::cout << " " << std::string(error.position, ' ') << "\033[1;31m^\033[0m\n\n";
}

std::cin.getline(input.data(), max_line - 1, '\n');
Expand Down
185 changes: 132 additions & 53 deletions src/libinfiz/Evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@
#include "Stack.hpp"
#include "StringTokenizer.hpp"
#include <concepts>
#include <string_view>
#include <expected>
#include <format>
#include <stdexcept>
#include <string_view>

enum struct Operators { PLUS_SIGN, CLOSE_PAREN, OPEN_PAREN, MINUS_SIGN, DIVIDE_SIGN, MULTIPLY_SIGN };

// Evaluation errors with position information
struct EvaluationError
{
enum class ErrorType {
INVALID_NUMBER,// Invalid character in number
INVALID_EXPRESSION,// General parsing error
EMPTY_TOKEN,// Empty token found
STACK_ERROR// Error with the stack operations
};

ErrorType type;
size_t position;// Position in the expression where the error occurred

constexpr EvaluationError(ErrorType t, size_t pos = 0) : type(t), position(pos) {}
};

constexpr auto precedence(Operators input) noexcept -> int
{
switch (input) {
Expand All @@ -29,7 +47,8 @@ constexpr auto precedence(Operators input) noexcept -> int
}


constexpr void evaluateStacks(Stack<RationalNumber> &numbers, Stack<Operators> &operators)
constexpr auto evaluateStacks(Stack<RationalNumber> &numbers, Stack<Operators> &operators)
-> std::expected<void, EvaluationError>
{
bool eatOpenParen = false;
bool cont = true;
Expand All @@ -54,85 +73,107 @@ constexpr void evaluateStacks(Stack<RationalNumber> &numbers, Stack<Operators> &

case Operators::PLUS_SIGN: {
operators.pop();
const auto operand2 = numbers.pop();
const auto operand1 = numbers.pop();
numbers.push(operand1 + operand2);
auto operand2_result = numbers.pop();
if (!operand2_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

auto operand1_result = numbers.pop();
if (!operand1_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

numbers.push(*operand1_result + *operand2_result);
break;
}

case Operators::MINUS_SIGN: {
operators.pop();
const auto operand2 = numbers.pop();
const auto operand1 = numbers.pop();
numbers.push(operand1 - operand2);
auto operand2_result = numbers.pop();
if (!operand2_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

auto operand1_result = numbers.pop();
if (!operand1_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

numbers.push(*operand1_result - *operand2_result);
break;
}

case Operators::MULTIPLY_SIGN: {
operators.pop();
const auto operand2 = numbers.pop();
const auto operand1 = numbers.pop();
numbers.push(operand1 * operand2);
auto operand2_result = numbers.pop();
if (!operand2_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

auto operand1_result = numbers.pop();
if (!operand1_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

numbers.push(*operand1_result * *operand2_result);
break;
}

case Operators::DIVIDE_SIGN: {
operators.pop();
const auto operand2 = numbers.pop();
const auto operand1 = numbers.pop();
numbers.push(operand1 / operand2);
auto operand2_result = numbers.pop();
if (!operand2_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

auto operand1_result = numbers.pop();
if (!operand1_result) { return std::unexpected(EvaluationError(EvaluationError::ErrorType::STACK_ERROR)); }

numbers.push(*operand1_result / *operand2_result);
break;
}

case Operators::CLOSE_PAREN:
break;// we want to continue
}
}

return {};
}


template<std::integral Type> [[nodiscard]] constexpr auto from_chars(std::string_view input) -> Type
template<std::integral Type>
[[nodiscard]] constexpr auto from_chars(std::string_view input) -> std::expected<Type, EvaluationError>
{
Type result{ 0 };

for (const char digit : input) {
for (size_t i = 0; i < input.size(); ++i) {
const char digit = input[i];
result *= 10;// NOLINT

if (digit >= '0' && digit <= '9') { result += static_cast<Type>(digit - '0'); } else {
throw std::range_error("not a number");
if (digit >= '0' && digit <= '9') {
result += static_cast<Type>(digit - '0');
} else {
// Return position information with the error
return std::unexpected(EvaluationError(EvaluationError::ErrorType::INVALID_NUMBER, i));
}
}

return result;
}

[[nodiscard]] constexpr auto evaluateExpression(StringTokenizer &tokenizer) -> RationalNumber
[[nodiscard]] constexpr auto evaluateExpression(StringTokenizer &tokenizer)
-> std::expected<RationalNumber, EvaluationError>
{
Stack<Operators> operators;
Stack<RationalNumber> numbers;

const auto throw_error = [&tokenizer] [[noreturn]] () {
throw std::runtime_error(std::format(
R"(Unable to evaluate expression
{}
{}^ unevaluated)",
tokenizer.input(),
std::string(tokenizer.offset(), ' ')));
// Creates an error with the current tokenizer position
const auto make_error = [&tokenizer](EvaluationError::ErrorType type) -> EvaluationError {
return EvaluationError(type, tokenizer.offset());
};

const auto evalStacks = [&]() {
try {
evaluateStacks(numbers, operators);
} catch (const std::runtime_error &) {
throw_error();
}
// Track parenthesis balance to detect mismatches
int parenthesis_count = 0;

const auto evalStacks = [&]() -> std::expected<void, EvaluationError> {
auto result = ::evaluateStacks(numbers, operators);
if (!result) { return std::unexpected(make_error(result.error().type)); }
return {};
};

while (tokenizer.hasMoreTokens()) {

auto next = tokenizer.nextToken();

if (next.empty()) { throw_error(); }
if (next.empty()) {
return std::unexpected(EvaluationError(EvaluationError::ErrorType::EMPTY_TOKEN, tokenizer.offset()));
}

auto value = Operators::PLUS_SIGN;

Expand All @@ -157,64 +198,102 @@ template<std::integral Type> [[nodiscard]] constexpr auto from_chars(std::string
break;
case ')':
value = Operators::CLOSE_PAREN;
parenthesis_count--;// Decrement for closing parenthesis
// Check for negative count (more closing than opening parentheses)
if (parenthesis_count < 0) {
return std::unexpected(
EvaluationError(EvaluationError::ErrorType::INVALID_EXPRESSION, tokenizer.offset() - 1));
}
operation = true;
break;
case '(':
value = Operators::OPEN_PAREN;
parenthesis_count++;// Increment open parenthesis count
operation = true;
break;

default:
operation = false;
try {
const std::integral auto parsed = from_chars<int>(next);
numbers.emplace(parsed, 1);
} catch (const std::range_error &) {
throw_error();
}
auto parsed_result = from_chars<int>(next);
if (!parsed_result) { return std::unexpected(parsed_result.error()); }
numbers.emplace(*parsed_result, 1);
break;
}

if (operation) {
switch (value) {
case Operators::OPEN_PAREN:
case Operators::OPEN_PAREN: {
operators.push(value);
break;
case Operators::CLOSE_PAREN:
}
case Operators::CLOSE_PAREN: {
// Check if the top of the stack is an opening parenthesis (empty parentheses case)
if (operators.peek() != nullptr && *operators.peek() == Operators::OPEN_PAREN) {
// Empty parentheses like () or part of (()) - not valid in this calculator
return std::unexpected(
EvaluationError(EvaluationError::ErrorType::INVALID_EXPRESSION, tokenizer.offset() - 1));
}

operators.push(value);
evalStacks();
auto eval_result = evalStacks();
if (!eval_result) { return std::unexpected(eval_result.error()); }
break;
default:
if (operators.peek() != nullptr && precedence(value) <= precedence(*operators.peek())) { evalStacks(); }
}
case Operators::PLUS_SIGN:
case Operators::MINUS_SIGN:
case Operators::MULTIPLY_SIGN:
case Operators::DIVIDE_SIGN: {
if (operators.peek() != nullptr && precedence(value) <= precedence(*operators.peek())) {
auto eval_result = evalStacks();
if (!eval_result) { return std::unexpected(eval_result.error()); }
}
operators.push(value);
break;
}
}
}
}
}

if (operators.peek() != nullptr) { evalStacks(); }
if (operators.peek() != nullptr) {
auto eval_result = evalStacks();
if (!eval_result) { return std::unexpected(eval_result.error()); }
}

if (!operators.empty() || tokenizer.hasUnparsedInput()) {
throw_error();
// Check for unbalanced parentheses
if (parenthesis_count > 0) {
return std::unexpected(EvaluationError(EvaluationError::ErrorType::INVALID_EXPRESSION, tokenizer.offset()));
}

if (numbers.peek() != nullptr) {
return *numbers.peek();
if (!operators.empty() || tokenizer.hasUnparsedInput()) {
return std::unexpected(EvaluationError(EvaluationError::ErrorType::INVALID_EXPRESSION, tokenizer.offset()));
}

throw_error();
if (numbers.peek() != nullptr) { return *numbers.peek(); }

return std::unexpected(EvaluationError(EvaluationError::ErrorType::INVALID_EXPRESSION, tokenizer.offset()));
}

[[nodiscard]] constexpr auto evaluate(std::string_view input) -> RationalNumber
[[nodiscard]] constexpr auto evaluate(std::string_view input) -> std::expected<RationalNumber, EvaluationError>
{
StringTokenizer tokenizer(input);
return evaluateExpression(tokenizer);
}

consteval auto evaluate_or_throw(std::string_view input) -> RationalNumber
{
StringTokenizer tokenizer(input);
auto result = evaluateExpression(tokenizer);
if (!result) {
// During compile-time evaluation, we can't do much better than a generic error message
throw std::runtime_error("Invalid expression during consteval");
}
return *result;
}

consteval auto operator""_rn(const char *str, std::size_t len) -> RationalNumber
{
return evaluate(std::string_view(str, len));
return evaluate_or_throw(std::string_view(str, len));
}


Expand Down
Loading