Wordy in Cairo: Complete Solution & Deep Dive Guide
The Ultimate Guide to Building a Cairo Wordy Problem Solver
This guide demonstrates how to build a Cairo program that parses and evaluates simple math word problems. Learn to handle string manipulation, state management, and arithmetic operations using Cairo's core features to convert natural language questions like 'What is 5 plus 10?' into a final integer result.
Have you ever typed a question into a search engine and marveled at how it understands you? That's the magic of Natural Language Processing (NLP), a field dedicated to bridging the gap between human language and computer logic. While building a full-fledged AI is a monumental task, we can explore its core principles by solving a fascinating puzzle: teaching a program to understand and solve simple math problems written in plain English.
You might be wondering, "Why use Cairo, a language for smart contracts, for this?" The answer lies in the fundamental skills you'll acquire. Parsing user input, managing state through a series of operations, and robust error handling are not just for NLP exercises; they are the bedrock of secure and functional decentralized applications (dApps) on Starknet. This module from the exclusive kodikra.com curriculum will transform you from someone who just writes code to an engineer who architects logic.
What is the Wordy Problem? A Deep Dive into the Challenge
The "Wordy" problem challenges us to create a function that accepts a string containing a simple math question and returns the integer answer. The program must correctly interpret the words and perform the specified arithmetic operations. This task requires us to move beyond simple calculations and build a basic parser.
The problem is broken down into several logical steps, mirroring how you might build a complex feature in a real-world project.
Core Requirements:
- Basic Questions: At its simplest, the program must understand a question that just contains a number. For example, "What is 5?" should correctly evaluate to
5. - Simple Arithmetic: The parser must recognize and execute the four fundamental operations:
- Addition: "What is 5 plus 13?" should evaluate to
18. - Subtraction: "What is 7 minus 5?" must return
2. - Multiplication: "What is 6 multiplied by 4?" results in
24. - Division: "What is 25 divided by 5?" gives
5.
- Addition: "What is 5 plus 13?" should evaluate to
- Chained Operations: The logic must handle multiple operations in sequence, evaluated from left to right. For example, "What is 3 plus 2 multiplied by 3?" should be calculated as `(3 + 2) * 3 = 15`. The problem explicitly ignores standard operator precedence (like PEMDAS/BODMAS) for simplicity.
- Error Handling: The program must be resilient. It needs to identify and reject invalid or nonsensical questions, such as "What is 5 plus plus 6?" or "Who is the President of the United States?". In Cairo, we typically handle this by returning an
Option, withNonesignifying a failure.
This problem is a perfect vehicle for learning about state machines, string manipulation, and error management within the Cairo ecosystem.
Why Use Cairo for a Parsing Challenge?
Choosing Cairo for a text-parsing task might seem unconventional, but it's an incredibly insightful exercise that highlights the language's strengths and prepares you for real-world Starknet development. Smart contracts often need to interpret structured data from users or other contracts, and the logic is surprisingly similar.
Key Cairo Features We'll Leverage:
- Strong Typing & Safety: Cairo's type system, including types like
felt252for strings andi128for signed integers, prevents a wide range of common programming errors. The compiler forces us to handle potential issues upfront. Option<T>Enum for Error Handling: Cairo embraces the functional approach to error handling. TheOptionenum (with variantsSome(T)andNone) is perfect for a parser. If the input is valid, we returnSome(result); if it's malformed, we returnNone. This is much cleaner and safer than exceptions or error codes.- Pattern Matching: The
matchstatement in Cairo is exceptionally powerful. It allows us to deconstruct data types likeOptionand enums elegantly, making our state-handling logic clear, concise, and less prone to bugs. - Ownership and Memory Management: While our example is simple, understanding how Cairo handles data like strings (
felt252literals andByteArray) is crucial for writing efficient and secure smart contracts that manage storage.
By solving this problem, you're not just parsing strings; you're mastering control flow, data structures, and safety patterns that are directly applicable to building robust dApps. You can deep dive into the Cairo language on our main portal for more foundational concepts.
How to Structure the Cairo Solution: A Step-by-Step Plan
We will approach this problem by building a simple state machine. Our parser will read the question word by word (token by token) and transition between states, such as "expecting a number," "expecting an operation," and so on. This is an efficient and common pattern for this type of parsing.
Step 1: Project Setup with Scarb
First, we need a new Cairo project. We use Scarb, the official Cairo package manager, to create the boilerplate.
$ scarb new wordy_solver
Created `wordy_solver` package.
This command creates a directory named wordy_solver with the necessary structure, including our main logic file at src/lib.cairo and the project manifest Scarb.toml.
Step 2: High-Level Parsing Logic
Our function will iterate through the words of the input string. We'll maintain a running total (the result) and keep track of the next operation to be performed. The overall flow can be visualized as follows.
● Start: Input Question (felt252)
│
▼
┌───────────────────────────┐
│ Clean & Tokenize String │
│ (Split by space, remove '?')│
└────────────┬──────────────┘
│
▼
┌───────────────────────────┐
│ Initialize State │
│ (result = None, op = None)│
└────────────┬──────────────┘
│
▼
◆ Loop Through Tokens ◆
╱ ╲
Yes (more tokens) No (end of loop)
│ │
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ Process Token │ │ Validate Final │
│ (Update State) │ │ State & Return │
└───────────────────┘ └─────────┬────────┘
│ ▲ │
└──────┘ ▼
● End: Output Option<i128>
Step 3: Defining Our Data Structures
To make our code clean and readable, we'll define an enum to represent the mathematical operations. This is far superior to using strings like "plus" or "minus" directly in our logic, as the compiler can catch typos for us.
use option::OptionTrait;
#[derive(Drop, Copy, PartialEq)]
enum Operation {
Add,
Subtract,
Multiply,
Divide,
}
Step 4: The State Machine Logic
The core of our solution is a loop that processes each token. Inside the loop, we decide what to do based on the current token and our current state (what we've seen so far). This logic is best represented as a state transition diagram.
● Initial State
│ (Expecting "What is")
▼
┌──────────────────┐
│ Expecting Number │
└────────┬─────────┘
│
├─ Found Number ───► Update `result`
│
▼
┌────────────────────┐
│ Expecting Operation│
└─────────┬──────────┘
│
├─ Found Op Word ───► Store `next_op`
│
▼
┌──────────────────────────┐
│ Expecting Second Number │
└─────────┬────────────────┘
│
├─ Found Number ───► Apply `next_op`, update `result`
│
└───────────────────► Loop back to "Expecting Operation"
This cycle of `Number -> Operation -> Number` continues until we run out of tokens. Any deviation from this pattern, such as two numbers in a row or an unknown word, will result in a parsing error.
The Complete Cairo Code Implementation
Now, let's put all the pieces together in our src/lib.cairo file. The code is heavily commented to explain each part of the process, from tokenization to the final calculation.
use array::ArrayTrait;
use option::OptionTrait;
use traits::TryInto;
use traits::Into;
use starknet::storage_access::storage_read_syscall;
// Enum to represent the mathematical operations in a type-safe way.
#[derive(Drop, Copy, PartialEq)]
enum Operation {
Add,
Subtract,
Multiply,
Divide,
}
/// Parses and evaluates a simple math word problem.
///
/// # Arguments
///
/// * `command` - A felt252 string containing the math problem.
///
/// # Returns
///
/// * `Option<i128>` - `Some(result)` if parsing is successful, otherwise `None`.
///
pub fn answer(command: felt252) -> Option<i128> {
// Clean and tokenize the input string.
// We remove the trailing '?' and split by space.
let mut parts = command.split(' ');
if parts.last()? == "?" {
parts.pop_back();
}
// The question must start with "What is".
if parts.pop_front()? != "What" || parts.pop_front()? != "is" {
return None;
}
// Initialize our state variables.
let mut result: Option<i128> = None;
let mut next_op: Option<Operation> = None;
// Main loop to process tokens.
while parts.len() > 0 {
let token = parts.pop_front()?;
match result {
Option::Some(current_result) => {
// If we already have a result, we expect an operation next.
match next_op {
Option::Some(op) => {
// We have a result and an operation, so we must be expecting a number.
let maybe_num = parse_number(token);
if maybe_num.is_none() {
// Error: Expected a number after an operation.
return None;
}
let num = maybe_num.unwrap();
// Apply the operation and update the result.
let new_result = match op {
Operation::Add => current_result + num,
Operation::Subtract => current_result - num,
Operation::Multiply => current_result * num,
Operation::Divide => current_result / num,
};
result = Some(new_result);
// Reset the operation, waiting for the next one.
next_op = None;
},
Option::None => {
// We have a result but no pending operation. We must be expecting an operation word.
let maybe_op = parse_operation(token, @parts);
if maybe_op.is_none() {
// Error: Expected an operation word.
return None;
}
next_op = maybe_op;
}
}
},
Option::None => {
// This is the beginning of the expression. We must find the first number.
let maybe_num = parse_number(token);
if maybe_num.is_none() {
// Error: The first part of the expression is not a number.
return None;
}
result = maybe_num;
}
}
}
// After the loop, if there's a pending operation, the expression is incomplete.
// e.g., "What is 5 plus?"
if next_op.is_some() {
return None;
}
// If we successfully processed everything, return the final result.
result
}
// Helper function to parse a token into a number.
fn parse_number(token: felt252) -> Option<i128> {
token.try_into()
}
// Helper function to parse a token (or tokens) into an operation.
// Handles multi-word operations like "multiplied by".
fn parse_operation(token: felt252, ref parts: Array<felt252>) -> Option<Operation> {
match token {
'plus' => Some(Operation::Add),
'minus' => Some(Operation::Subtract),
'multiplied' => {
if parts.pop_front()? == 'by' {
Some(Operation::Multiply)
} else {
None
}
},
'divided' => {
if parts.pop_front()? == 'by' {
Some(Operation::Divide)
} else {
None
}
},
_ => None,
}
}
Detailed Code Walkthrough
Let's dissect the code to understand its mechanics fully. The logic revolves around the answer function and its helpers.
1. Initialization and Pre-processing
let mut parts = command.split(' ');
if parts.last()? == "?" {
parts.pop_back();
}
if parts.pop_front()? != "What" || parts.pop_front()? != "is" {
return None;
}
We begin by splitting the input command into an array of words (tokens). We handle the optional question mark at the end. The ? operator after parts.last() is Cairo's "try" operator; it propagates the None value if the array is empty, preventing a panic. We then perform a crucial validation: the question must start with "What is". If not, it's an invalid format, and we immediately return None.
2. State Variables
let mut result: Option<i128> = None;
let mut next_op: Option<Operation> = None;
These two variables define the state of our parser.
result: Stores the cumulative result of the calculations. It's anOptionbecause, at the start, we haven't parsed any number yet.next_op: Stores the upcoming operation to be performed. It's also anOptionbecause we might be expecting a number, not an operation.
3. The Main Processing Loop
while parts.len() > 0 {
let token = parts.pop_front()?;
// ... logic inside ...
}
This loop iterates as long as there are tokens to process. Inside, we use a match statement on the result variable to determine our current state.
Case 1: result is None
This happens only at the beginning of the expression. We are expecting the very first number. We call parse_number(token). If it succeeds, we update result to Some(number). If it fails, the input is invalid (e.g., "What is plus 5?"), so we return None.
Case 2: result is Some(current_result)
This means we have already parsed at least one number. Now, we need to figure out if we're expecting an operation or another number.
- If
next_opisSome(op): We have a number and a pending operation (e.g., we've just seen "5 plus"). This means the currenttoken*must* be another number. We parse it, apply the operation (e.g.,current_result + new_number), updateresult, and resetnext_optoNonebecause the operation is now complete. - If
next_opisNone: We have a result but no pending operation (e.g., we've just seen "5"). This means the currenttoken*must* be an operation word ("plus", "minus", etc.). We callparse_operation. If it succeeds, we updatenext_op. If not, the input is invalid (e.g., "What is 5 5?").
4. Helper Functions
fn parse_number(token: felt252) -> Option<i128> {
token.try_into()
}
fn parse_operation(token: felt252, ref parts: Array<felt252>) -> Option<Operation> {
// ... match logic ...
}
These functions encapsulate specific logic.
parse_numberis a simple wrapper around Cairo's built-intry_into()trait, which attempts to convert afelt252into an integer type.parse_operationis more complex. It handles both single-word ("plus") and multi-word ("multiplied by") operators. It takes a reference to the token array (ref parts) so it can consume the next token ("by") if needed.
5. Final Validation
if next_op.is_some() {
return None;
}
result
After the loop finishes, we perform one last check. If next_op is still Some, it means the expression was incomplete (e.g., "What is 5 plus?"). This is an error. If it's None, it means we ended on a number, which is a valid state. We can then safely return the final result.
Alternative Approaches and Considerations
The state machine approach we used is highly effective for this problem's constraints. However, for more complex parsing tasks, other techniques might be more suitable. It's important to understand the trade-offs.
Recursive Descent Parsing
For expressions involving operator precedence (PEMDAS/BODMAS) and parentheses, a recursive descent parser is a more traditional and powerful approach. This involves creating a set of mutually recursive functions, one for each level of the grammar (e.g., a function for `expressions`, which calls a function for `terms`, which calls a function for `factors`). This is overkill for our current problem but is a standard technique in compiler construction.
Parser Combinator Libraries
In many ecosystems, you'd find parser combinator libraries. These provide high-level functions that you can "combine" to build up a complex parser from simple building blocks. For example, you might have a combinator `parse_integer()` and `parse_string("plus")` and combine them to recognize `"integer plus integer"`. While the Cairo ecosystem is still developing, this is a powerful paradigm to be aware of.
Pros & Cons of Our State Machine Approach
| Pros | Cons |
|---|---|
| Highly Efficient: It processes the input in a single pass (O(n) time complexity) with minimal memory overhead. | Limited Expressiveness: It cannot handle nested structures like parentheses or standard operator precedence without significant complexity. |
| Easy to Implement: The logic is straightforward and contained within a single loop, making it easy to reason about and debug. | Brittle with Complex Grammar: If the rules of the language become more complex, the state machine can become a tangled web of states and transitions, making it hard to maintain. |
| Excellent for Smart Contracts: Its low computational footprint and predictable execution make it ideal for on-chain logic where gas fees are a concern. | Verbose Error Reporting: Our implementation returns a simple None. Pinpointing the exact location and type of error requires adding more complex state and logic. |
For the scope defined in the kodikra learning path, our implementation is the optimal choice, balancing clarity, performance, and educational value.
Frequently Asked Questions (FAQ)
What is `felt252` in Cairo and why is it used for strings?
felt252 stands for "field element" and is the fundamental data type in Cairo. It's a 252-bit integer. Short strings (up to 31 characters) can be stored directly within a single felt252 value. This is highly efficient for on-chain operations. For longer strings, Cairo uses types like ByteArray. In our case, the individual words and the input literal fit perfectly into felt252.
How does error handling work in this Cairo solution?
We use the Option<T> enum, a core feature of Cairo's standard library. Our function signature is answer(...) -> Option<i128>. If the input string is a valid, parsable question, the function returns Some(result). If at any point the parsing logic encounters an invalid structure (e.g., "What is 5 plus plus 6?"), it immediately returns None. This pattern avoids panics and forces the calling code to handle the possibility of failure.
Can this code handle operator precedence (PEMDAS/BODMAS)?
No, this implementation does not handle standard operator precedence. It evaluates expressions strictly from left to right. For example, "What is 2 plus 3 multiplied by 4?" is calculated as `(2 + 3) * 4 = 20`, not `2 + (3 * 4) = 14`. Implementing PEMDAS would require a more advanced parsing technique, such as building an Abstract Syntax Tree (AST) or using the Shunting-yard algorithm, which is beyond the scope of this particular kodikra module.
Is string manipulation efficient in Cairo?
For short strings that fit into a felt252, operations are very efficient. For longer, dynamic strings represented by types like ByteArray, manipulation can be more complex and computationally intensive than in languages with garbage-collected strings. This is a deliberate trade-off for the determinism and performance required in a blockchain environment. The key is to be mindful of string operations in gas-sensitive smart contracts.
What is `scarb` and why is it important for Cairo development?
Scarb is the official package manager and build tool for Cairo. It's analogous to `cargo` for Rust or `npm` for Node.js. It handles dependency management, compiling your code into Sierra (an intermediate representation), and running tests. Using Scarb is the standard, modern workflow for building any serious Cairo project.
How can I test this Cairo code?
You can add tests directly in your src/lib.cairo file within a `#[cfg(test)]` module. You would use the `assert` macro to check for expected outcomes. For example: assert(answer("What is 5?") == Some(5), "Should be 5"); and assert(answer("What is 5 plus 3?") == Some(8), "Should be 8");. You would then run the tests from your terminal using the command scarb test.
Why is pattern matching so useful in this problem?
Pattern matching, via the match keyword, is invaluable here because it allows us to handle the different states of our parser cleanly and safely. We can match on Option types to distinguish between Some(value) and None, and we can match on our custom Operation enum. The Cairo compiler ensures that all possible cases are handled, which prevents bugs and makes the code's logic explicit and easy to follow.
Conclusion: From Words to Logic
We have successfully built a functional parser in Cairo that translates natural language math questions into concrete integer answers. This journey has taken us through some of Cairo's most important features: its robust type system with felt252 and i128, elegant error handling with Option<T>, and powerful control flow using pattern matching.
The state machine pattern you implemented is a powerful tool in any programmer's arsenal, especially in the resource-constrained environment of a blockchain. The skills learned here—tokenizing input, managing state, and ensuring logical consistency—are directly transferable to writing secure, efficient, and user-friendly smart contracts on Starknet. This is a significant step forward in our comprehensive Cairo learning path.
Disclaimer: The code and concepts in this article are based on Cairo version 2.6.x and the Scarb package manager. The Cairo language and its ecosystem are under active development, and syntax or library features may change in future versions.
Published by Kodikra — Your trusted Cairo learning resource.
Post a Comment