Skip to content

Commit

Permalink
Sections written for calculator evaluation
Browse files Browse the repository at this point in the history
  • Loading branch information
Caleb-o committed Sep 21, 2024
1 parent bd867da commit 1a41c40
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 4 deletions.
5 changes: 3 additions & 2 deletions examples/projects/calculator.c3
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module project::calculator;

import std::io;
import std::core::string;
import std::core::mem;
Expand Down Expand Up @@ -124,7 +125,7 @@ fn Token! Parser.make_number(&self) {
self.advance(); // Increment as we know the first character

// Consume numbers
while (!self.at_end() && range.contains(self.source[self.ip])) {
while (!self.at_end() && range.contains(self.peek())) {
self.advance();
}

Expand All @@ -133,7 +134,7 @@ fn Token! Parser.make_number(&self) {
self.advance();

// Consume numbers again
while (!self.at_end() && range.contains(self.source[self.ip])) {
while (!self.at_end() && range.contains(self.peek())) {
self.advance();
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
- [Using stdio]()
- [Filesystem]()
- [Echo Client/Server]()
- [Calculator Evaluator]()
- [Calculator Evaluator](./projects/calculator.md)
2 changes: 1 addition & 1 deletion src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Topics we will cover:
- [Using stdio](): reading and writing to the console with stdio
- [The Filesystem](): reading and writing files
- [TCP Server](): a basic echo TCP client and server
- [Evaluating a basic expression](): create a small parser and evaluator for a calculator
- [Evaluating a basic expression](./projects/calculator.md): create a small parser and evaluator for a calculator
104 changes: 104 additions & 0 deletions src/projects/calculator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Calculator Evaluator

> 🚧 This is a work in progress
We are going to create a small project, that can lex, parse and evaluate an expression for a basic calculator. We will try and keep this nice and easy and so we are going to merge a few things together for simplicity. You can see the full example at [the end](#final-code).

## Getting Started

With this example, we can just use a single script that I am going to call `calculator.c3`. If you want to use a project, you can do `c3c init calculator`, move into the directory and open `src/main.c3` in your editor.

To begin, we will import everything the project requires up front, so we don't need to worry about this later. We will also write up our main function too:

```c++
{{#include ../../examples/projects/calculator.c3::20}}
```
We import all the necessary modules required for the project, which includes allocators, string functions, io for printing and the range type just to make lexing numbers simpler. In our main function, we create an `expression` variable, which will be the code our evaluator runs. Next we pass it to the `calculate` function, which returns an optional `float`, as this can fail. We handle the error case, then print the result if all goes well. This will make more sense once we get to implementing the code.
> I've opted to write my own lexer and parser, as using a library is overkill.
> Writing your own lexer and parser is also fun and can also be quite simple.
### Tokens
Tokens are a small structure that are used to annotate pieces of code. In a programming language, these can be things like a keyword, operators, identifiers etc. Since we're making a calculator, this structure will be pretty simple, as we only have operators and numbers.
```c++
{{#include ../../examples/projects/calculator.c3:32:44}}
```

Our `TokenKind` is a simple enum, which will denote what the token represents. We also include `EOF` as a way to know we're done. The `Token` is also quite simple in this case, we have the `kind` and the `lexeme`. The lexeme will be a slice that we will use to parse the `float` for our numbers. We can also use this to print nicer error messages if we wanted to.

### The "Lexer"

Our lexer is the system that will take our source and convert it into a stream of tokens. This is the first merge we will do with our types, where it will actually act as the parser as well. For such a small example like our calculator, this is fine, but you might want to have a lexer and parser seperately for more serious projects. Here is how we will define the lexer/parser combo:

```c++
{{#include ../../examples/projects/calculator.c3:26:30}}
```
Quite a small structure too, as it takes our source, the `ip` for keeping track of where we are in the source, then a `Token` that we will use later for parsing. Let's create a small helper function to create a parser for us:
```c++
{{#include ../../examples/projects/calculator.c3:65:67}}
```

We simply pass the source in, then 0 out the rest of the fields. We put a dummy token in for the `current` token field, as we will set it later. Before we write the lexing function, we will setup a few methods to make lexing easier.

```c++
{{#include ../../examples/projects/calculator.c3:69:89}}
```
These are some pretty simple methods, to help us get the current char, advance, check if we're at the end of the source and for skipping whitespace.
> In a more complex lexer, we would account for tabs, newlines, breaks etc in our `skip_whitespace`. We might also have variable `peek` to look-ahead as well as viewing the current char.
Now for the meat of our lexer, the function that will generate a token. For our calculator, we can write this pretty easily.
```c++
{{#include ../../examples/projects/calculator.c3:91:121}}
```

This will skip whitespace, if we're at the end, then generate an `EOF` token. Then we check the current `char` and see whether it's an operator, or if it's a number. Otherwise, we will return a fault of `BAD_SYNTAX`. You may also notice the `make_number` method and we will implement that shortly, but let us create the fault.

```c++
{{#include ../../examples/projects/calculator.c3:22:24}}
```
Nothing too crazy for our fault here. We will also re-use this in our parser, when we get an unexpected symbol. Back to the lexer! We can now look at lexing a number:
```c++
{{#include ../../examples/projects/calculator.c3:123:143}}
```

Almost the same size as our `get_token` method itself! We capture the current index of our source and then advance, as we have already checked the first character. We then advance as long as there is a number as the current char. Then we check for a `.` to support basic decimal numbers, then advance again for trailing numbers. After that, we then slice our source to capture the number, using the `start` variable we created for our anchor.

That's the end of our lexing methods and now we head over to parsing.

### Parsing Expressions

## Result

Running the project:
```sh
$ c3c run
```

Using the compiler directly:
```sh
$ c3c compile-run --run-once calculator.c3
```

Output:
```sh
Program linked to executable 'calculator'.
Launching ./calculator
Result = 4.400000
Program completed with exit code 0.
```

## Final Code

```c++
{{#include ../../examples/projects/calculator.c3}}
```
Binary file added src/projects/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 1a41c40

Please sign in to comment.