Skip to content

Commit e8d2a65

Browse files
committed
Editorial revision
1 parent 1aab658 commit e8d2a65

50 files changed

Lines changed: 224 additions & 349 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

book/src/01_calculator/ast.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ This handles `-1` (minus applied to 1) or `-(2 + 3)` (minus applied to a whole e
5656
**Binary expressions** - An operator between two things:
5757

5858
```
59-
BinaryExpr = { Term ~ (Operator ~ Term)* }
59+
BinaryExpr = { (UnaryExpr | Term) ~ (Operator ~ Term)+ }
6060
```
6161

62-
This handles `1 + 2` (plus between 1 and 2) or `1 + 2 + 3` (chained additions).
62+
This handles `1 + 2` (plus between 1 and 2) or `1 + 2 + 3` (chained additions). The `+` (one or more) requires at least one operator, which keeps a lone `Term` from matching as a binary expression.
6363

6464
Here's a complex example: `"-1 + (2 + 3)"` forms this tree:
6565

@@ -117,7 +117,7 @@ The core insight is **recursion**. To evaluate `1 + 2`:
117117
2. Evaluate the right side (`2`) → get `2`
118118
3. Apply the operator (`+`) → get `3`
119119

120-
If the left side were `(3 + 4)` instead of `1`, we'd recursively evaluate that first. This is why trees are so powerful - the structure tells us the order of operations.
120+
If the left side were `(3 + 4)` instead of `1`, we'd recursively evaluate that first. This is what the tree buys us: the structure itself encodes the order of operations.
121121

122122
Here's the evaluation function:
123123

@@ -161,7 +161,7 @@ Run tests locally with:
161161
cargo test interpreter --tests
162162
```
163163

164-
## Why This Pattern Matters
164+
## The Tree-Walking Pattern
165165

166166
The pattern we just learned - parse to AST, recursively evaluate - is the foundation of *every* interpreter. Python, Ruby, JavaScript interpreters all do this (with more node types, of course).
167167

@@ -170,7 +170,7 @@ In the next sections, we'll see two other ways to execute the same AST:
170170
- **JIT compilation** - Convert the AST to machine code, then run it
171171
- **Bytecode VM** - Convert to simpler instructions, then interpret those
172172

173-
Same AST, three different backends. That's the power of separating parsing from execution.
173+
Same AST, three different backends. That is what separating parsing from execution gives us.
174174

175175
<div class="checkpoint">
176176

book/src/01_calculator/ast_traversal.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,20 @@ Let's trace through what happens when we JIT `1 + 2`:
5757

5858
2. **Setup LLVM** - Create context, module, builder
5959

60-
3. **Create wrapper function** - We need a function to call, so we create `__jit` with signature `() -> i64`
60+
3. **Create wrapper function** - We need a function to call, so we create `jit` with signature `() -> i32`
6161

6262
4. **Compile AST** - `recursive_builder` walks the tree:
63-
* Compile `Int(1)` → creates i64 constant `1`
64-
* Compile `Int(2)` → creates i64 constant `2`
65-
* Compile `Add` → creates `add i64 1, 2` instruction, returns the result
63+
* Compile `Int(1)` → creates i32 constant `1`
64+
* Compile `Int(2)` → creates i32 constant `2`
65+
* Compile `Add` → creates `add i32 1, 2` instruction, returns the result
6666

6767
5. **Return result** - `build_return` emits a `ret` instruction with our computed value
6868

6969
6. **JIT compile** - LLVM turns our IR into native machine code
7070

7171
7. **Execute** - Call the function, get `3`
7272

73-
### Why This Matters
73+
### Scaling to Any Expression
7474

7575
The recursive builder pattern scales to any expression, no matter how complex:
7676

book/src/01_calculator/basic_llvm.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Walking through this:
8383

8484
### Step 3: Build the Function Body
8585

86-
Now the fun part - generating the actual addition:
86+
Now we generate the actual addition:
8787

8888
```rust,ignore
8989
{{#include ../../../calculator/examples/llvm/src/main.rs:third}}
@@ -106,7 +106,7 @@ Time to turn our IR into machine code and run it:
106106
{{#include ../../../calculator/examples/llvm/src/main.rs:fourth}}
107107
```
108108

109-
This is where it gets real:
109+
Here the IR becomes executable code:
110110

111111
- **`module.create_jit_execution_engine(OptimizationLevel::None)`** - Creates a JIT compiler. LLVM takes our IR and compiles it to native x86/ARM code *right now*, in memory.
112112
- **`execution_engine.get_function::<unsafe extern "C" fn(i32, i32) -> i32>("add")`** - Look up our compiled function. The type signature tells Rust how to call it.

book/src/01_calculator/debugging.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,29 @@ The verifier catches:
127127

128128
Always verify before JIT execution!
129129

130+
## How Errors Surface Today
131+
132+
Our languages report errors as values, not exceptions. Each stage hands back a `Result`: the calculator uses `anyhow::Result<T>`, while Firstlang, Secondlang, and Thirdlang return `Result<T, String>`. There are three kinds you will meet:
133+
134+
- **Parse errors** come from pest and already carry a line and column:
135+
136+
```text
137+
Parse error: --> 8:9
138+
|
139+
8 | def classify(self) -> int {
140+
| ^---
141+
|
142+
= expected Identifier
143+
```
144+
145+
- **Type errors** come from the type checker as plain strings, for example `Type mismatch: expected int, got bool`.
146+
147+
- **Setup and I/O errors**, such as pointing the CLI at a file that does not exist.
148+
149+
The CLIs for Firstlang, Secondlang, and Thirdlang print these with `eprintln!` and exit non-zero. The calculator is the exception: its entry point still `unwrap()`s the file read and the parse result, so a missing file or a syntax error there ends in a panic rather than a tidy message.
150+
151+
Two limits are worth naming. First, type-error strings have no source location, so they tell you *what* is wrong but not *where*. Second, the first error stops the pipeline; you fix one, rerun, and find the next. Real compilers attach a span to every diagnostic and recover to report several at once. We sketch how to get there in [What's Next](../whats_next.md#path-4-better-error-handling).
152+
130153
## The Debugging Mindset
131154

132155
> Think like a detective. You have a crime (wrong output). You need to find where in the pipeline the crime occurred. Interrogate each stage until you find the culprit.

book/src/01_calculator/grammar_lexer_parser.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Don't let this overwhelm you - let's break it down line by line:
3131

3232
**`Program = _{ SOI ~ Expr ~ EOF }`** - A program is an expression, surrounded by "start of input" (`SOI`) and "end of file" (`EOF`). The `_{ }` means we don't create a node for `Program` itself - it's just a wrapper.
3333

34-
**`Expr = { UnaryExpr | BinaryExpr | Term }`** - An expression is either unary (`-1`), binary (`1 + 2`), or a simple term (just a number). The `|` means "or" - try each alternative in order.
34+
**`Expr = { BinaryExpr | UnaryExpr | Term }`** - An expression is either binary (`1 + 2`), unary (`-1`), or a simple term (just a number). The `|` means "or", and PEG tries each alternative in order, so `BinaryExpr` is attempted first.
3535

3636
**`Term = { Int | "(" ~ Expr ~ ")" }`** - A term is either a number or a parenthesized expression. This is how we handle `(1 + 2) * 3` - the parenthesized part becomes a single term.
3737

@@ -67,9 +67,9 @@ CalcParser::parse(Rule::Program, source)
6767

6868
This returns a tree of "pairs" - pest's way of representing matched rules. In the [next section](./ast.md), we'll convert these pairs into our own AST structure, which is easier to work with.
6969

70-
## Why This Matters
70+
## Why Not Just Use Regexes?
7171

72-
You might wonder: why not just use regular expressions? For a simple calculator, you probably could. But as languages get more complex (nested expressions, functions, classes), grammars scale and regexes don't.
72+
For a simple calculator, you probably could use regular expressions. But as languages get more complex (nested expressions, functions, classes), grammars scale and regexes don't.
7373

7474
The grammar is also your language's specification. When someone asks "is `--1` valid?", you look at the grammar. When you add a new feature, you extend the grammar. It's the single source of truth for what your language accepts.
7575

book/src/01_calculator/vm.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This is how Python, Java, Ruby, and many other languages work. You compile sourc
1212

1313
### What Is Bytecode?
1414

15-
Think of bytecode as a *simplified assembly language* designed for our VM. Real assembly has hundreds of instructions. Our bytecode has just four:
15+
Think of bytecode as a *simplified assembly language* designed for our VM. Real assembly has hundreds of instructions. These four carry the arithmetic:
1616

1717
| Opcode | What it does |
1818
|--------|--------------|
@@ -21,7 +21,7 @@ Think of bytecode as a *simplified assembly language* designed for our VM. Real
2121
| `OpSub` | Pop two values, push their difference |
2222
| `OpPop` | Pop and discard the top value |
2323

24-
That's it! With just these four operations, we can evaluate any arithmetic expression.
24+
The full enum also has `OpPlus` and `OpMinus` for unary expressions, but these four are enough to evaluate any binary arithmetic expression.
2525

2626
## Step 1: Define Opcodes
2727

book/src/02_firstlang/control_flow.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ max(10, 20) # 20
8585
max(20, 10) # 20
8686
```
8787

88-
The condition `a > b` determines which value to return. Simple, but powerful.
88+
The condition `a > b` determines which value to return.
8989

9090
## Loops: While
9191

@@ -191,7 +191,7 @@ Same result, different approach. Loops and recursion are often interchangeable.
191191

192192
## Control Flow in Functions
193193

194-
The real power comes from combining everything. Here's a more complex example:
194+
Combining these constructs is where real programs take shape. Here's a more complex example:
195195

196196
```
197197
def countdown(n) {
@@ -243,7 +243,7 @@ find_first_even(5) # 2
243243

244244
When `i = 2`, the condition `i % 2 == 0` is true, and we return immediately. The loop doesn't continue to `i = 3, 4, 5`. This "early return" pattern is common and efficient.
245245

246-
## What Happens Under the Hood
246+
## Control Flow as Branching
247247

248248
Both `if` and `while` are about *changing the flow of execution*. Without them, we execute line by line. With them, we can:
249249

book/src/02_firstlang/fibonacci.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ This is simple, but let's trace through what happens when we call `fib(4)`:
4747

4848
Each call creates a new stack frame in our interpreter, and when the function returns, we pop the frame and continue with the result.
4949

50-
## Why This Works
50+
## How Our Interpreter Handles This
5151

5252
Our interpreter properly handles recursion because:
5353

book/src/02_firstlang/functions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ When we call `outer()`:
150150

151151
The stack grows when functions are called and shrinks when they return. This is why we call it a *stack* - last in, first out.
152152

153-
## Why Frames Matter
153+
## One Frame Per Call
154154

155155
Consider this code:
156156

@@ -220,4 +220,4 @@ The call stack during `quadruple(5)`:
220220

221221
Notice how `x` has different values in different frames, even though they're all named `x`.
222222

223-
Next, we'll add [control flow](./control_flow.md) to make our functions more powerful - the ability to make decisions and repeat actions.
223+
Next, we'll add [control flow](./control_flow.md): the ability to make decisions and repeat actions.

book/src/02_firstlang/intro.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ The Calculator grammar was minimal:
1212

1313
```text
1414
Program = _{ SOI ~ Expr ~ EOF }
15-
Expr = { UnaryExpr | BinaryExpr | Term }
16-
Term = _{Int | "(" ~ Expr ~ ")" }
15+
Expr = { BinaryExpr | UnaryExpr | Term }
16+
Term = { Int | "(" ~ Expr ~ ")" }
1717
UnaryExpr = { Operator ~ Term }
18-
BinaryExpr = { Term ~ (Operator ~ Term)+ }
18+
BinaryExpr = { (UnaryExpr | Term) ~ (Operator ~ Term)+ }
1919
Operator = { "+" | "-" }
20-
Int = @{ Operator? ~ ASCII_DIGIT+ }
20+
Int = @{ ASCII_DIGIT+ }
2121
```
2222

2323
Firstlang adds *statements*, *identifiers*, *functions*, and *control flow*:

0 commit comments

Comments
 (0)