Skip to content

Semantic Analysis

What Is Semantic Analysis?

Imagine you write an essay and run spell-check. It catches misspelt words and broken grammar, but it can't tell you whether your sentences make sense. You could write "The cat drove the ocean to school" -- every word is spelt right and the grammar is fine, but the meaning is nonsense.

The semantic analyzer is the sense-checker for your program. (Semantic means "about meaning".) The parser already checked the grammar (syntax), but the analyzer walks through the AST and asks: "Does this program actually make logical sense?"

Why the Parser Isn't Enough

The parser makes sure your code looks right:

let x = 5
print(y)

The parser is perfectly happy with this -- every token is in the right place. But wait: we never declared y! The program's grammar is fine, but its meaning is broken. That's where the analyzer steps in and says: "Hold on, y doesn't exist."

Here are some things the parser cannot catch:

Problem Example
Using a variable that doesn't exist print(y) when y was never declared
Declaring the same variable twice let x = 1 then let x = 2 in the same block
Calling a function that doesn't exist foo() when foo was never defined
Wrong number of arguments add(1) when add takes two parameters
Returning outside a function return 42 at the top level

Scopes: Rooms in a Building

A scope is like a room in a building. When you declare a variable, you're writing its name on a whiteboard in that room. Anyone in the room can see it.

let x = 10

if true {
    let y = 20
    print(x)    # Can see x (it's in the hallway)
    print(y)    # Can see y (it's in this room)
}

print(x)        # Fine -- x is still on the hallway whiteboard
print(y)        # Error! y's whiteboard is in the if-room, and we left

The rules are simple:

  • You can always look outward -- from a room into the hallway, from the hallway into the lobby
  • You can never look inward -- from the hallway into a room
  • Sibling rooms can't see each other -- the if room and the else room are separate

What Creates a New Scope?

Construct New scope for...
if { } The if-body
else { } The else-body
while { } The loop body
for i in ... { } The loop variable and body
fn f() { } The parameters and body

Variable Checks

Declaration

When you write let x = 5, the analyzer writes x on the current room's whiteboard. If x is already there, it raises an error:

let x = 1
let x = 2    # Error: Variable 'x' already declared

But if you're in a different room (an inner scope), you can use the same name. This is called shadowing -- your local x temporarily hides the outer one:

let x = 1
if true {
    let x = 99   # Fine -- this is a different room
    print(x)     # Prints 99
}
print(x)         # Prints 1 -- the outer x was never changed

Using a Variable

When you use a variable name, the analyzer checks: "Is this name on any whiteboard I can see?" It starts in the current room and walks outward through every enclosing scope until it reaches the global scope. If it never finds the name, you get an error -- like trying to pick up a package that isn't addressed to you:

print(z)    # Error: Undeclared variable 'z'

Reassignment

Reassignment (x = 10) only works if x was already declared somewhere visible. You can't reassign a variable that doesn't exist:

x = 5    # Error: Undeclared variable 'x'

Function Checks

Declaring a Function

When you write fn greet() { ... }, the analyzer registers greet as a function. Each room actually has two whiteboards -- one for variables and one for functions. That means you could have a variable called greet and a function called greet without conflict.

Just like variables, you can't declare two functions with the same name in the same scope:

fn greet() { print("hi") }
fn greet() { print("bye") }   # Error: Function 'greet' already defined

Calling a Function

When you call a function, the analyzer checks two things:

  1. Does the function exist? If you call foo() but never defined foo, that's an error.
  2. Did you pass the right number of arguments? Think of it like a recipe that needs exactly 2 eggs -- you can't use 1 or 3. If add takes two parameters but you only pass one, that's an error.
fn add(a, b) {
    return a + b
}

add(1)        # Error: Function 'add' expects 2 arguments, got 1
add(1, 2, 3)  # Error: Function 'add' expects 2 arguments, got 3
add(1, 2)     # Fine!

When a function has default parameters, the analyzer accepts a range of argument counts -- from the number of required parameters up to the total. For example, fn f(a, b = 10) accepts 1 or 2 arguments.

Define Before You Call

Pebble reads your program from top to bottom. You must define a function before you call it:

greet()                    # Error: Undeclared function 'greet'
fn greet() { print("hi") }

Built-in Functions

Several functions are built-in -- they're already on the whiteboard when your program starts. You never need to declare them. The full list is in Standard Library, but the most common ones are:

print("hello")           # Works -- print is built in
for i in range(10) { }   # Works -- range is built in
let n = len([1, 2, 3])   # Works -- len is built in

Return Checks

A return statement can only appear inside a function. Using it at the top level is like trying to "send back" an answer when nobody asked you a question -- there's no function to return from:

return 42    # Error: Return statement outside function

Inside a function, you can return from any depth of nesting:

fn check(x) {
    if x > 0 {
        return x     # Fine -- we're inside fn check
    }
    return 0
}

Block Scoping in Action

Here's a bigger example showing how scopes nest:

let total = 0                # Global scope

fn process(n) {              # New scope: n is a parameter
    for i in range(n) {      # New scope: i is the loop variable
        if i > 0 {           # New scope
            let temp = i     # Only visible inside this if-block
            total = total + temp
        }
    }
    # i is gone here (left the for-room)
    # temp is gone here (left the if-room)
    # n is still visible (we're still in the function)
}

Error Examples

Code Error
print(x) Undeclared variable 'x'
let x = 1 then let x = 2 Variable 'x' already declared
x = 5 (no let) Undeclared variable 'x'
foo() Undeclared function 'foo'
fn f(a) {} then f(1, 2) Function 'f' expects 1 argument, got 2
return 42 (top level) Return statement outside function
let x = x Undeclared variable 'x' (x isn't declared yet when the value is evaluated)
const x = 1 then x = 2 Cannot reassign constant 'x'
break (outside loop) Break statement outside loop
continue (outside loop) Continue statement outside loop
await expr (not in async fn) await can only be used inside an async function
yield (inside async fn) Cannot use yield inside an async function

Summary

Concept Analogy
Semantic analysis A teacher checking your essay for sense, not just spelling
Scope A room in a building with whiteboards
Declaration Writing a name on the whiteboard
Using a variable Looking for a name on whiteboards you can see
Shadowing An inner room's whiteboard hiding an outer one
Undeclared variable Trying to pick up a package that isn't addressed to you
Argument count A recipe that needs exactly 2 eggs -- you can't use 1 or 3
Return outside function Trying to "send back" when nobody asked you a question