Skip to content

Commit 2de34ae

Browse files
lefticusclaude
andcommitted
Refactor: Replace exceptions with std::expected
- Replaced all exception handling with C++23 std::expected for better error handling - Added custom error types with contextual error messages - Created evaluate_or_throw for constexpr evaluation - Updated tests to work with std::expected return types - Added new tests to verify error handling behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ffd2a28 commit 2de34ae

File tree

5 files changed

+199
-63
lines changed

5 files changed

+199
-63
lines changed

src/infiz/infiz.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ auto main() -> int
1515
std::cin.getline(input.data(), max_line - 1, '\n');
1616

1717
while (std::cin.good()) {
18-
try {
19-
const auto answer = evaluate(input.data());
18+
auto result = evaluate(input.data());
19+
20+
if (result) {
21+
const auto answer = *result;
2022
std::cout << "answer: ";
2123

2224
if (answer.getDenominator() == 1) {
@@ -25,8 +27,8 @@ auto main() -> int
2527
std::cout << std::format(
2628
"{}/{} ({})\n", answer.getNumerator(), answer.getDenominator(), answer.asFloat<double>());
2729
}
28-
} catch (const std::runtime_error &err) {
29-
std::cout << err.what() << '\n';
30+
} else {
31+
std::cout << result.error().message << '\n';
3032
}
3133

3234
std::cin.getline(input.data(), max_line - 1, '\n');

src/libinfiz/Evaluator.hpp

Lines changed: 151 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,25 @@
77
#include <concepts>
88
#include <string_view>
99
#include <format>
10+
#include <expected>
11+
#include <stdexcept>
1012

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

15+
struct EvaluationError {
16+
enum class ErrorType {
17+
INVALID_NUMBER,
18+
INVALID_EXPRESSION,
19+
EMPTY_TOKEN,
20+
STACK_EMPTY
21+
};
22+
23+
ErrorType type;
24+
std::string message;
25+
26+
constexpr EvaluationError(ErrorType t, std::string_view msg) : type(t), message(msg) {}
27+
};
28+
1329
constexpr auto precedence(Operators input) noexcept -> int
1430
{
1531
switch (input) {
@@ -29,7 +45,8 @@ constexpr auto precedence(Operators input) noexcept -> int
2945
}
3046

3147

32-
constexpr void evaluateStacks(Stack<RationalNumber> &numbers, Stack<Operators> &operators)
48+
constexpr auto evaluateStacks(Stack<RationalNumber> &numbers, Stack<Operators> &operators)
49+
-> std::expected<void, EvaluationError>
3350
{
3451
bool eatOpenParen = false;
3552
bool cont = true;
@@ -54,85 +71,148 @@ constexpr void evaluateStacks(Stack<RationalNumber> &numbers, Stack<Operators> &
5471

5572
case Operators::PLUS_SIGN: {
5673
operators.pop();
57-
const auto operand2 = numbers.pop();
58-
const auto operand1 = numbers.pop();
59-
numbers.push(operand1 + operand2);
74+
auto operand2_result = numbers.pop();
75+
if (!operand2_result) {
76+
return std::unexpected(EvaluationError(
77+
EvaluationError::ErrorType::STACK_EMPTY,
78+
std::format("Missing second operand for + operation: {}", operand2_result.error().message)));
79+
}
80+
81+
auto operand1_result = numbers.pop();
82+
if (!operand1_result) {
83+
return std::unexpected(EvaluationError(
84+
EvaluationError::ErrorType::STACK_EMPTY,
85+
std::format("Missing first operand for + operation: {}", operand1_result.error().message)));
86+
}
87+
88+
numbers.push(*operand1_result + *operand2_result);
6089
break;
6190
}
6291

6392
case Operators::MINUS_SIGN: {
6493
operators.pop();
65-
const auto operand2 = numbers.pop();
66-
const auto operand1 = numbers.pop();
67-
numbers.push(operand1 - operand2);
94+
auto operand2_result = numbers.pop();
95+
if (!operand2_result) {
96+
return std::unexpected(EvaluationError(
97+
EvaluationError::ErrorType::STACK_EMPTY,
98+
std::format("Missing second operand for - operation: {}", operand2_result.error().message)));
99+
}
100+
101+
auto operand1_result = numbers.pop();
102+
if (!operand1_result) {
103+
return std::unexpected(EvaluationError(
104+
EvaluationError::ErrorType::STACK_EMPTY,
105+
std::format("Missing first operand for - operation: {}", operand1_result.error().message)));
106+
}
107+
108+
numbers.push(*operand1_result - *operand2_result);
68109
break;
69110
}
70111

71112
case Operators::MULTIPLY_SIGN: {
72113
operators.pop();
73-
const auto operand2 = numbers.pop();
74-
const auto operand1 = numbers.pop();
75-
numbers.push(operand1 * operand2);
114+
auto operand2_result = numbers.pop();
115+
if (!operand2_result) {
116+
return std::unexpected(EvaluationError(
117+
EvaluationError::ErrorType::STACK_EMPTY,
118+
std::format("Missing second operand for * operation: {}", operand2_result.error().message)));
119+
}
120+
121+
auto operand1_result = numbers.pop();
122+
if (!operand1_result) {
123+
return std::unexpected(EvaluationError(
124+
EvaluationError::ErrorType::STACK_EMPTY,
125+
std::format("Missing first operand for * operation: {}", operand1_result.error().message)));
126+
}
127+
128+
numbers.push(*operand1_result * *operand2_result);
76129
break;
77130
}
78131

79132
case Operators::DIVIDE_SIGN: {
80133
operators.pop();
81-
const auto operand2 = numbers.pop();
82-
const auto operand1 = numbers.pop();
83-
numbers.push(operand1 / operand2);
134+
auto operand2_result = numbers.pop();
135+
if (!operand2_result) {
136+
return std::unexpected(EvaluationError(
137+
EvaluationError::ErrorType::STACK_EMPTY,
138+
std::format("Missing second operand for / operation: {}", operand2_result.error().message)));
139+
}
140+
141+
auto operand1_result = numbers.pop();
142+
if (!operand1_result) {
143+
return std::unexpected(EvaluationError(
144+
EvaluationError::ErrorType::STACK_EMPTY,
145+
std::format("Missing first operand for / operation: {}", operand1_result.error().message)));
146+
}
147+
148+
numbers.push(*operand1_result / *operand2_result);
84149
break;
85150
}
86151

87152
case Operators::CLOSE_PAREN:
88153
break;// we want to continue
89154
}
90155
}
156+
157+
return {};
91158
}
92159

93160

94-
template<std::integral Type> [[nodiscard]] constexpr auto from_chars(std::string_view input) -> Type
161+
template<std::integral Type>
162+
[[nodiscard]] constexpr auto from_chars(std::string_view input) -> std::expected<Type, EvaluationError>
95163
{
96164
Type result{ 0 };
97165

98166
for (const char digit : input) {
99167
result *= 10;// NOLINT
100168

101-
if (digit >= '0' && digit <= '9') { result += static_cast<Type>(digit - '0'); } else {
102-
throw std::range_error("not a number");
169+
if (digit >= '0' && digit <= '9') {
170+
result += static_cast<Type>(digit - '0');
171+
} else {
172+
return std::unexpected(EvaluationError(
173+
EvaluationError::ErrorType::INVALID_NUMBER,
174+
std::format("Invalid digit '{}' in number", digit)));
103175
}
104176
}
105177

106178
return result;
107179
}
108180

109-
[[nodiscard]] constexpr auto evaluateExpression(StringTokenizer &tokenizer) -> RationalNumber
181+
[[nodiscard]] constexpr auto evaluateExpression(StringTokenizer &tokenizer)
182+
-> std::expected<RationalNumber, EvaluationError>
110183
{
111184
Stack<Operators> operators;
112185
Stack<RationalNumber> numbers;
113186

114-
const auto throw_error = [&tokenizer] [[noreturn]] () {
115-
throw std::runtime_error(std::format(
187+
const auto make_error = [&tokenizer](EvaluationError::ErrorType type, std::string_view msg = "") -> EvaluationError {
188+
std::string contextual_message = std::format(
116189
R"(Unable to evaluate expression
117190
{}
118191
{}^ unevaluated)",
119192
tokenizer.input(),
120-
std::string(tokenizer.offset(), ' ')));
193+
std::string(tokenizer.offset(), ' '));
194+
195+
if (!msg.empty()) {
196+
contextual_message = std::format("{}: {}", contextual_message, msg);
197+
}
198+
199+
return EvaluationError(type, contextual_message);
121200
};
122201

123-
const auto evalStacks = [&]() {
124-
try {
125-
evaluateStacks(numbers, operators);
126-
} catch (const std::runtime_error &) {
127-
throw_error();
202+
const auto evalStacks = [&]() -> std::expected<void, EvaluationError> {
203+
auto result = ::evaluateStacks(numbers, operators);
204+
if (!result) {
205+
return std::unexpected(make_error(result.error().type, result.error().message));
128206
}
207+
return {};
129208
};
130209

131210
while (tokenizer.hasMoreTokens()) {
132-
133211
auto next = tokenizer.nextToken();
134212

135-
if (next.empty()) { throw_error(); }
213+
if (next.empty()) {
214+
return std::unexpected(make_error(EvaluationError::ErrorType::EMPTY_TOKEN));
215+
}
136216

137217
auto value = Operators::PLUS_SIGN;
138218

@@ -166,55 +246,84 @@ template<std::integral Type> [[nodiscard]] constexpr auto from_chars(std::string
166246

167247
default:
168248
operation = false;
169-
try {
170-
const std::integral auto parsed = from_chars<int>(next);
171-
numbers.emplace(parsed, 1);
172-
} catch (const std::range_error &) {
173-
throw_error();
249+
auto parsed_result = from_chars<int>(next);
250+
if (!parsed_result) {
251+
return std::unexpected(make_error(parsed_result.error().type, parsed_result.error().message));
174252
}
253+
numbers.emplace(*parsed_result, 1);
175254
break;
176255
}
177256

178257
if (operation) {
179258
switch (value) {
180-
case Operators::OPEN_PAREN:
259+
case Operators::OPEN_PAREN: {
181260
operators.push(value);
182261
break;
183-
case Operators::CLOSE_PAREN:
262+
}
263+
case Operators::CLOSE_PAREN: {
184264
operators.push(value);
185-
evalStacks();
265+
auto eval_result = evalStacks();
266+
if (!eval_result) {
267+
return std::unexpected(eval_result.error());
268+
}
186269
break;
187-
default:
188-
if (operators.peek() != nullptr && precedence(value) <= precedence(*operators.peek())) { evalStacks(); }
270+
}
271+
case Operators::PLUS_SIGN:
272+
case Operators::MINUS_SIGN:
273+
case Operators::MULTIPLY_SIGN:
274+
case Operators::DIVIDE_SIGN: {
275+
if (operators.peek() != nullptr && precedence(value) <= precedence(*operators.peek())) {
276+
auto eval_result = evalStacks();
277+
if (!eval_result) {
278+
return std::unexpected(eval_result.error());
279+
}
280+
}
189281
operators.push(value);
190282
break;
191283
}
284+
}
192285
}
193286
}
194287
}
195288

196-
if (operators.peek() != nullptr) { evalStacks(); }
289+
if (operators.peek() != nullptr) {
290+
auto eval_result = evalStacks();
291+
if (!eval_result) {
292+
return std::unexpected(eval_result.error());
293+
}
294+
}
197295

198296
if (!operators.empty() || tokenizer.hasUnparsedInput()) {
199-
throw_error();
297+
return std::unexpected(make_error(EvaluationError::ErrorType::INVALID_EXPRESSION));
200298
}
201299

202300
if (numbers.peek() != nullptr) {
203301
return *numbers.peek();
204302
}
205303

206-
throw_error();
304+
return std::unexpected(make_error(EvaluationError::ErrorType::INVALID_EXPRESSION));
207305
}
208306

209-
[[nodiscard]] constexpr auto evaluate(std::string_view input) -> RationalNumber
307+
[[nodiscard]] constexpr auto evaluate(std::string_view input) -> std::expected<RationalNumber, EvaluationError>
210308
{
211309
StringTokenizer tokenizer(input);
212310
return evaluateExpression(tokenizer);
213311
}
214312

313+
consteval auto evaluate_or_throw(std::string_view input) -> RationalNumber
314+
{
315+
StringTokenizer tokenizer(input);
316+
auto result = evaluateExpression(tokenizer);
317+
if (!result) {
318+
// During compile-time evaluation, we can't do much better than a generic error message
319+
throw std::runtime_error("Invalid expression during consteval");
320+
}
321+
return *result;
322+
}
323+
215324
consteval auto operator""_rn(const char *str, std::size_t len) -> RationalNumber
216325
{
217-
return evaluate(std::string_view(str, len));
326+
return evaluate_or_throw(std::string_view(str, len));
218327
}
219328

220329

src/libinfiz/Stack.hpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
#include <cassert>
66
#include <cstddef>
77
#include <vector>
8-
#include <stdexcept>
8+
#include <expected>
9+
#include <string>
10+
#include <format>
911

12+
struct StackError {
13+
std::string message;
14+
explicit constexpr StackError(std::string_view msg) : message(msg) {}
15+
};
1016

1117
template<typename Contained> class Stack
1218
{
@@ -15,9 +21,11 @@ template<typename Contained> class Stack
1521

1622
[[nodiscard]] constexpr auto empty() const noexcept -> bool { return data.empty(); }
1723

18-
constexpr auto pop() -> Contained
24+
constexpr auto pop() -> std::expected<Contained, StackError>
1925
{
20-
if (data.empty()) { throw std::runtime_error("No elements left to pop!"); }
26+
if (data.empty()) {
27+
return std::unexpected(StackError("No elements left to pop!"));
28+
}
2129
Contained toReturn = data.back();
2230
data.pop_back();
2331
return toReturn;

0 commit comments

Comments
 (0)