Variables and Assignment
Variables let us store values and refer to them by name. Without variables, we could only work with literal values - every computation would need to repeat its inputs. Variables give us memory.
x = 42
name = x + 8 # name = 50
That second line is the key: we’re using x by name instead of repeating 42. If we later change x, code that uses x automatically uses the new value. Variables make code reusable and readable.
How Variables Work
Let’s trace through what happens when our interpreter runs x = 42; y = x + 1:
Step 1: Parse to AST
The parser turns this into:
[
Stmt::Assignment { name: "x", value: Expr::Int(42) },
Stmt::Assignment { name: "y", value: Expr::Binary {
op: Add,
left: Expr::Var("x"),
right: Expr::Int(1)
}},
]
Notice Expr::Var("x") - that’s a reference to a variable, not the variable itself. We’ll need to look it up later.
Step 2: Execute First Assignment
For x = 42:
- Evaluate the right side -
Expr::Int(42)evaluates toValue::Int(42) - Store in the environment - Put
"x" -> 42in our storage
Step 3: Execute Second Assignment
For y = x + 1:
- Evaluate the right side - This is a binary expression, so:
- Evaluate left:
Expr::Var("x")→ look up “x” in storage → find42 - Evaluate right:
Expr::Int(1)→1 - Apply operator:
42 + 1 = 43
- Evaluate left:
- Store in the environment - Put
"y" -> 43in our storage
The Environment (Storage)
Where do variables live? In a HashMap inside a “frame”:
struct Frame {
locals: HashMap<String, Value>,
}
A HashMap is a dictionary: given a key (the variable name), it returns a value. When we assign x = 42, we insert ("x", 42) into the map. When we look up x, we query the map with "x" and get 42 back.
Why call it a “frame”? Because in the functions chapter, we’ll have multiple frames - one for each function call. Each function gets its own private storage.
Variable Lookup
When we encounter a variable like x, we need to find its value:
fn lookup_var(&self, name: &str) -> Result<Value, String> {
// First, check the current frame
if let Some(value) = self.current_frame().locals.get(name) {
return Ok(value.clone());
}
// Then, check globals (for functions defined at top level)
if let Some(value) = self.globals.get(name) {
return Ok(value.clone());
}
// Not found anywhere - error!
Err(format!("Undefined variable: {}", name))
}
This is called scoping: we first look in the local scope (current function), then fall back to global scope. If the variable isn’t anywhere, that’s an error.
Scoping: Local vs Global
Variables can be local (inside a function) or global (outside all functions):
x = 10 # global - accessible everywhere
def foo() {
y = 20 # local to foo - only accessible inside foo
return x + y # can access global x
}
foo() # = 30
# y # ERROR: y is not defined here
When foo runs, it has access to:
- Its own locals (
y) - Global variables (
x)
But code outside foo cannot see y - it only existed during the function call.
Reassignment
Variables can be reassigned:
x = 1
x = x + 1 # x is now 2
x = x * 2 # x is now 4
The right side is always evaluated first, using the current value of x. Then the result overwrites x. So x = x + 1 means “take the current x, add 1, store it back in x.”
Why This Matters
This simple mechanism - storing and looking up names - is the foundation of all programming:
- Parameters are just variables that get their values from function calls
- Loop counters are variables that change each iteration
- Object fields (in Thirdlang) are variables attached to objects
The environment is one of the most important data structures in any interpreter.
Example
# Variables and arithmetic
a = 5
b = 3
sum = a + b # 8
diff = a - b # 2
prod = a * b # 15
# Reassignment
x = 1
x = x + 1
x = x * 2
x # 4
Trace through this mentally:
- After line 1:
{a: 5} - After line 2:
{a: 5, b: 3} - After line 3:
{a: 5, b: 3, sum: 8}(computed 5 + 3) - And so on…
At this point, you should be able to:
- Parse and evaluate
x = 42andy = x + 1 - Look up variables and get their values
- Handle “undefined variable” errors gracefully
In the next section, we’ll see how functions use these building blocks - parameters are just local variables that get their values from call sites.