Skip to content

Commit

Permalink
add Calculator conundrum concept exercise (#307)
Browse files Browse the repository at this point in the history
* non-working scaffold

* Implement exercise + update prerequisites

* recreate exercise + add design.md + add panic to exerc

* division panics

* add hints

* add introduction

* add instructions

* remove ? section

* make operation simple ByteArray (no Option)

* fix hints

* Lint markdown

* Apply suggestions from code review

Co-authored-by: András B Nagy <[email protected]>

---------

Co-authored-by: András B Nagy <[email protected]>
  • Loading branch information
0xNeshi and BNAndras authored Dec 6, 2024
1 parent b754ae2 commit 01caba2
Show file tree
Hide file tree
Showing 21 changed files with 288 additions and 8 deletions.
18 changes: 18 additions & 0 deletions concepts/error-handling/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ There is also a special macro, called `panic!`, that allows a `ByteArray` to be
panic!("The error for the panic! Error message is not limited to 31 characters anymore");
```

### The `assert!` Macro

The `assert!` macro is a useful tool for enforcing specific conditions in your code.
If the condition in `assert!` evaluates to `false`, the program will panic with a `ByteArray` error message.
This is often used to verify assumptions during development and ensure values meet certain criteria.

For example:

```rust
fn main() {
let x = 5;
assert!(x > 0, "x must be greater than zero");
}
```

If `x` is not greater than zero, the program will panic with the message `"x must be greater than zero"`.
`assert!` is helpful for checking invariants and preconditions without manually writing error-handling code.

### `nopanic` Notation

Cairo `nopanic` notation indicates that a function will never panic.
Expand Down
4 changes: 4 additions & 0 deletions concepts/error-handling/links.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
{
"url": "https://book.cairo-lang.org/ch09-00-error-handling.html",
"description": "Error handling in the Cairo book"
},
{
"url": "https://book.cairo-lang.org/ch11-05-macros.html?highlight=assert#assert-and-assert_xx-macros",
"description": "assert! macro"
}
]
9 changes: 5 additions & 4 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,15 @@
"status": "wip"
},
{
"slug": "the-farm",
"name": "The Farm",
"uuid": "e167e30c-84b1-44da-b21c-70bc46688f20",
"slug": "calculator-conundrum",
"name": "Calculator Conundrum",
"uuid": "989df4b1-6d89-468d-96e0-47013e3cf99b",
"concepts": [
"error-handling"
],
"prerequisites": [
"structs"
"traits",
"option"
],
"status": "wip"
},
Expand Down
25 changes: 25 additions & 0 deletions exercises/concept/calculator-conundrum/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Hints

## General

- [Unrecoverable Errors][unrecoverable]: invoke a panic with error message.
- [`panic!` Macro][panic-excl-macro]: invoke a panic with `ByteArray` error message.
- [`assert!` Macro][assert]: invoke a panic if a condition evaluates to `false`.
- [Result Enum][result]: how to use the `Result` enum.

## 2. Handle illegal operations

- You need to [return an error][result] here.

## 3. Handle no operation provided

- You need to [panic][panic-excl-macro] here with a `ByteArray` message to handle empty strings for operations.

## 4. Handle errors when dividing by zero

- You need to panic here to handle division by zero.

[unrecoverable]: https://book.cairo-lang.org/ch09-01-unrecoverable-errors-with-panic.html#unrecoverable-errors-with-panic
[panic-excl-macro]: https://book.cairo-lang.org/ch09-01-unrecoverable-errors-with-panic.html#panic-macro
[assert]: https://book.cairo-lang.org/ch11-05-macros.html?highlight=assert#assert-and-assert_xx-macros
[result]: https://book.cairo-lang.org/ch09-02-recoverable-errors.html#the-result-enum
46 changes: 46 additions & 0 deletions exercises/concept/calculator-conundrum/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Instructions

In this exercise, you will be building error handling for a simple integer calculator.
The calculator should support addition, multiplication, and division operations, returning the result as a formatted string.
You will also implement error handling to address illegal operations and division by zero.

The goal is to have a working calculator that returns a string in the following format:

```rust
SimpleCalculator::calculate(16, 51, "+"); // => returns "16 + 51 = 67"

SimpleCalculator::calculate(32, 6, "*"); // => returns "32 * 6 = 192"

SimpleCalculator::calculate(512, 4, "/"); // => returns "512 / 4 = 128"
```

## 1. Implement the calculator operations

The main function for this task will be `SimpleCalculator::calculate`, which takes three arguments: two integers and a `ByteArray` representing the operation.
Implement the following operations:

- **Addition** with the `+` symbol
- **Multiplication** with the `*` symbol
- **Division** with the `/` symbol

## 2. Handle illegal operations

If the operation symbol is anything other than `+`, `*`, or `/`, the calculator should either panic or return an error:

- If the operation is an empty string, panic with a `ByteArray` error message `"Operation cannot be an empty string"`.
- For any other invalid operation, return `Result::Err("Operation is out of range")`.

```rust
SimpleCalculator::calculate(100, 10, "-"); // => returns Result::Err("Operation is out of range")

SimpleCalculator::calculate(8, 2, ""); // => panics with "Operation cannot be an empty string"
```

## 3. Handle errors when dividing by zero

When attempting to divide by `0`, the calculator should panic with an error message indicating that division by zero is not allowed.
The returned result should be a `felt252` value of `'Division by zero is not allowed'`.

```rust
SimpleCalculator::calculate(512, 0, "/"); // => panics with 'Division by zero is not allowed'
```
65 changes: 65 additions & 0 deletions exercises/concept/calculator-conundrum/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Introduction

In programming, it's essential to handle errors gracefully to ensure that unexpected situations do not cause a program to crash or behave unpredictably.
Cairo provides two main mechanisms for error handling:

1. **Unrecoverable errors** with `panic`, which immediately stop the program.
2. **Recoverable errors** with `Result`, which allow the program to handle and respond to errors.

## Unrecoverable Errors with `panic`

Sometimes, a program encounters an error so severe that it cannot proceed.
Cairo uses the `panic` function to immediately stop execution in such cases.
This is helpful when an error, like attempting to access an out-of-bounds index, makes it impossible for the program to continue in a sensible way.
The `panic` function accepts a `ByteArray` message that describes the reason for the error.

For example, in Cairo:

```rust
fn main() {
let data = array![1, 2];
panic("An unrecoverable error has occurred!");
}
```

This example demonstrates a forced panic, which immediately stops the program with a message.

### The `assert!` Macro

The `assert!` macro is a useful tool for enforcing specific conditions in your code.
If the condition in `assert!` evaluates to `false`, the program will panic with a `ByteArray` error message.
This is often used to verify assumptions during development and ensure values meet certain criteria.

For example:

```rust
fn main() {
let x = 5;
assert!(x > 0, "x must be greater than zero");
}
```

If `x` is not greater than zero, the program will panic with the message `"x must be greater than zero"`. `assert!` is helpful for checking invariants and preconditions without manually writing error-handling code.

## Recoverable Errors with `Result`

Not all errors need to stop the program.
Some can be handled gracefully so the program can continue.
Cairo's `Result` enum represents these recoverable errors, and it has two variants:

- `Result::Ok` indicates success.
- `Result::Err` represents an error.

Using `Result`, a function can return either a success value or an error, allowing the calling function to decide what to do next.

```rust
fn divide(a: u32, b: u32) -> Result<u32, ByteArray> {
if b == 0 {
Result::Err("Error: Division by zero")
} else {
Result::Ok(a / b)
}
}
```

In this example, if `b` is zero, an error is returned; otherwise, the result of the division is returned.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"authors": [
"<your_gh_username>"
"0xNeshi"
],
"files": {
"solution": [
"src/lib.cairo"
],
"test": [
"tests/the_farm.cairo"
"tests/calculator_conundrum.cairo"
],
"exemplar": [
".meta/exemplar.cairo"
Expand All @@ -17,7 +17,7 @@
]
},
"forked_from": [
"go/the-farm"
"csharp/calculator-conundrum"
],
"blurb": "<blurb>"
"blurb": "Learn about error handling by working on a simple calculator."
}
27 changes: 27 additions & 0 deletions exercises/concept/calculator-conundrum/.meta/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Design

## Goal

Introduce the student to error handling in Cairo.

## Learning objectives

- know how create recoverable errors.
- know how create unrecoverable errors.

## Out of scope

## Concepts

- error-handling

## Prerequisites

- traits
- option

## Resources to refer to

- [Cairo Book - Error Handling][error-handling]

[error-handling]: https://book.cairo-lang.org/ch09-00-error-handling.html
17 changes: 17 additions & 0 deletions exercises/concept/calculator-conundrum/.meta/exemplar.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#[generate_trait]
pub impl SimpleCalculatorImpl of SimpleCalculatorTrait {
fn calculate(a: i32, b: i32, operation: ByteArray) -> Result<ByteArray, ByteArray> {
assert!(operation != "", "Operation cannot be an empty string");

if operation == "+" {
Result::Ok(format!("{} + {} = {}", a, b, a + b))
} else if operation == "*" {
Result::Ok(format!("{} * {} = {}", a, b, a * b))
} else if operation == "/" {
assert(b != 0, 'Division by zero is not allowed');
Result::Ok(format!("{} / {} = {}", a, b, a / b))
} else {
Result::Err("Operation is out of range")
}
}
}
7 changes: 7 additions & 0 deletions exercises/concept/calculator-conundrum/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "calculator_conundrum"
version = "0.1.0"
edition = "2024_07"

[dev-dependencies]
cairo_test = "2.8.2"
6 changes: 6 additions & 0 deletions exercises/concept/calculator-conundrum/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[generate_trait]
pub impl SimpleCalculatorImpl of SimpleCalculatorTrait {
fn calculate(a: i32, b: i32, operation: ByteArray) -> Result<ByteArray, ByteArray> {
panic!("implement `calculate`")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use calculator_conundrum::SimpleCalculatorTrait as SimpleCalculator;

#[test]
fn addition_with_small_operands() {
assert_eq!(SimpleCalculator::calculate(22, 25, "+").unwrap(), "22 + 25 = 47");
}

#[test]
#[ignore]
fn addition_with_large_operands() {
assert_eq!(
SimpleCalculator::calculate(378_961, 399_635, "+").unwrap(), "378961 + 399635 = 778596"
);
}

#[test]
#[ignore]
fn multiplication_with_small_operands() {
assert_eq!(SimpleCalculator::calculate(3, 21, "*").unwrap(), "3 * 21 = 63");
}

#[test]
#[ignore]
fn multiplication_with_large_operands() {
assert_eq!(
SimpleCalculator::calculate(72_441, 2_048, "*").unwrap(), "72441 * 2048 = 148359168"
);
}

#[test]
#[ignore]
fn division_with_small_operands() {
assert_eq!(SimpleCalculator::calculate(72, 9, "/").unwrap(), "72 / 9 = 8");
}

#[test]
#[ignore]
fn division_with_large_operands() {
assert_eq!(
SimpleCalculator::calculate(1_338_800, 83_675, "/").unwrap(), "1338800 / 83675 = 16"
);
}

#[test]
#[ignore]
fn calculate_returns_result_err_for_non_valid_operations() {
assert_eq!(SimpleCalculator::calculate(1, 2, "**").unwrap_err(), "Operation is out of range");
}

#[test]
#[ignore]
#[should_panic(expected: ("Operation cannot be an empty string",))]
fn calculate_returns_result_err_for_empty_string_as_operation() {
let _ = SimpleCalculator::calculate(1, 2, "");
}

#[test]
#[ignore]
#[should_panic(expected: ('Division by zero is not allowed',))]
fn calculate_panics_for_division_with_0() {
let _ = SimpleCalculator::calculate(33, 0, "/");
}
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions single-sentence-per-line-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ module.exports = {
"e.g",
"etc",
"ex",
"`assert",
"`panic",
];
const lineEndings = params.config.line_endings || [".", "?", "!"];
const sentenceStartRegex =
Expand Down

0 comments on commit 01caba2

Please sign in to comment.