You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
63
63
64
64
Here's a complex example: `"-1 + (2 + 3)"` forms this tree:
65
65
@@ -117,7 +117,7 @@ The core insight is **recursion**. To evaluate `1 + 2`:
117
117
2. Evaluate the right side (`2`) → get `2`
118
118
3. Apply the operator (`+`) → get `3`
119
119
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.
121
121
122
122
Here's the evaluation function:
123
123
@@ -161,7 +161,7 @@ Run tests locally with:
161
161
cargo test interpreter --tests
162
162
```
163
163
164
-
## Why This Pattern Matters
164
+
## The Tree-Walking Pattern
165
165
166
166
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).
167
167
@@ -170,7 +170,7 @@ In the next sections, we'll see two other ways to execute the same AST:
170
170
-**JIT compilation** - Convert the AST to machine code, then run it
171
171
-**Bytecode VM** - Convert to simpler instructions, then interpret those
172
172
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.
-**`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.
112
112
-**`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.
Copy file name to clipboardExpand all lines: book/src/01_calculator/debugging.md
+23Lines changed: 23 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -127,6 +127,29 @@ The verifier catches:
127
127
128
128
Always verify before JIT execution!
129
129
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
+
130
153
## The Debugging Mindset
131
154
132
155
> 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.
Copy file name to clipboardExpand all lines: book/src/01_calculator/grammar_lexer_parser.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,7 +31,7 @@ Don't let this overwhelm you - let's break it down line by line:
31
31
32
32
**`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.
33
33
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.
35
35
36
36
**`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.
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.
69
69
70
-
## Why This Matters
70
+
## Why Not Just Use Regexes?
71
71
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.
73
73
74
74
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.
Copy file name to clipboardExpand all lines: book/src/01_calculator/vm.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,7 +12,7 @@ This is how Python, Java, Ruby, and many other languages work. You compile sourc
12
12
13
13
### What Is Bytecode?
14
14
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:
16
16
17
17
| Opcode | What it does |
18
18
|--------|--------------|
@@ -21,7 +21,7 @@ Think of bytecode as a *simplified assembly language* designed for our VM. Real
21
21
|`OpSub`| Pop two values, push their difference |
22
22
|`OpPop`| Pop and discard the top value |
23
23
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.
Copy file name to clipboardExpand all lines: book/src/02_firstlang/control_flow.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -85,7 +85,7 @@ max(10, 20) # 20
85
85
max(20, 10) # 20
86
86
```
87
87
88
-
The condition `a > b` determines which value to return. Simple, but powerful.
88
+
The condition `a > b` determines which value to return.
89
89
90
90
## Loops: While
91
91
@@ -191,7 +191,7 @@ Same result, different approach. Loops and recursion are often interchangeable.
191
191
192
192
## Control Flow in Functions
193
193
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:
195
195
196
196
```
197
197
def countdown(n) {
@@ -243,7 +243,7 @@ find_first_even(5) # 2
243
243
244
244
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.
245
245
246
-
## What Happens Under the Hood
246
+
## Control Flow as Branching
247
247
248
248
Both `if` and `while` are about *changing the flow of execution*. Without them, we execute line by line. With them, we can:
0 commit comments