Modules
The Sharing Machine
Imagine you and your friends each have a toolbox. Every time you start a new project, you don't buy a brand-new set of hammers, saws, and screwdrivers -- you borrow the tools you need from a friend's toolbox. That way, everyone builds their own tools once, and the whole group benefits.
A module in Pebble is like a friend's toolbox. It's a .pbl
file full of functions and structs that you can borrow into your
own program.
Why Modules?
Without modules, every program would have to contain everything it
needs. If you wrote a handy add function in one project and wanted
to use it in another, you'd have to copy-paste it. That leads to:
- Repetition -- the same code in ten different files.
- Mistakes -- fix a bug in one copy but forget the other nine.
- Messiness -- one giant file that's hard to read.
Modules solve all three problems. Write it once, import it anywhere.
Importing Everything
Use import followed by a path in quotes:
This brings in every function, struct, and class that helpers.pbl
defines. It's the easiest way to grab a whole toolbox at once.
Importing Specific Names
If you only need a few tools, use from ... import:
Names you don't list stay out of your program. This is like saying "I only need the hammer and the tape measure, thanks."
What Gets Imported
Only definitions travel between files:
fnfunction definitionsstructstruct definitionsclassclass definitionsenumenum definitions
Executable statements like print(...), let x = ..., or loops
do not run when you import a module. The module's code is read
for its blueprints, not executed line by line.
# noisy.pbl
print("I am noisy!") # this does NOT execute during import
fn quiet() { return 0 } # this IS available to importers
File Paths
Import paths are strings that point to .pbl files. They are
resolved relative to the file that contains the import:
Creating a Module
Any .pbl file is automatically a module. Just write functions and
structs:
Then another file can import it:
Nested Imports
Modules can import other modules. If a.pbl imports b.pbl, and
b.pbl imports c.pbl, everything works automatically:
# c.pbl
fn base() { return 1 }
# b.pbl
import "c.pbl"
fn mid() { return base() + 10 }
# a.pbl
import "b.pbl"
fn top() { return mid() + 100 }
Pebble handles the chain for you and never compiles the same module twice.
Built-in Modules
Not every module is a .pbl file. Pebble comes with two built-in
modules that are always available to import — no file needed:
"math"— numeric functions likesqrt,abs,floor, and constants likepiande"io"— theinput()function for reading from the keyboard
These use exactly the same import / from ... import syntax as
file modules. The resolver checks for built-in module names first;
if there's no match, it looks for a .pbl file.
See Standard Library for the full list of built-in module functions and constants.
Import Rules
-
Imports must come first. All
importandfrom ... importlines must appear at the very top of your file, before any other statements. -
No circular imports. If
a.pblimportsb.pbl, thenb.pblcannot importa.pbl(directly or through a chain). Pebble detects this and reports an error. -
No
exportkeyword. Every top-levelfnandstructis automatically available to importers. -
Imported names go into your scope. There are no namespaces or prefixes --
addfrommath.pblis just calledadd.
Error Cases
Pebble catches import problems early:
# File not found
import "nonexistent.pbl"
# Error: Module 'nonexistent.pbl' not found
# Circular import
# a.pbl imports b.pbl, b.pbl imports a.pbl
# Error: Circular import detected: 'a.pbl'
# Name not exported
from "math.pbl" import multiply
# Error: Module 'math.pbl' does not export 'multiply'
# Import after code
let x = 1
import "math.pbl"
# Error: Imports must appear at the top of the file
Practical Examples
Helper Library
# helpers.pbl
fn add(a, b) { return a + b }
fn sub(a, b) { return a - b }
fn mul(a, b) { return a * b }
Geometry with Structs
# geometry.pbl
struct Point { x, y }
fn distance(p) {
return (p.x ** 2 + p.y ** 2) ** 0.5
}
fn translate(p, dx, dy) {
return Point(p.x + dx, p.y + dy)
}
import "geometry.pbl"
let p = Point(3, 4)
print(distance(p)) # 5.0
let moved = translate(p, 1, 1)
print(moved) # Point(x=4, y=5)
How It Works Under the Hood
When Pebble sees an import statement, here's what happens at
compile time (before any code runs):
flowchart TD
A["import \"math.pbl\""] --> B[Resolve path relative to current file]
B --> C{Already compiled?}
C -- Yes --> D[Use cached result]
C -- No --> E{Circular?}
E -- Yes --> F[Error: circular import]
E -- No --> G[Read file]
G --> H[Lex → Parse → Resolve nested imports → Analyze → Compile]
H --> I[Extract exported fn/struct names]
I --> J[Cache result]
J --> K[Register names in importer's scope]
K --> L[Merge all functions/structs into CompiledProgram]
The key insight: no new bytecode opcodes are needed. The resolver
compiles each module independently, extracts its function and struct
definitions, and merges them into the importing program's
CompiledProgram. At runtime, calling an imported function is
exactly like calling a local one -- the VM doesn't know the
difference.
Summary
| Syntax | What it does |
|---|---|
import "path.pbl" |
Bring in all functions, structs, classes, and enums from a file module |
from "path.pbl" import name |
Bring in specific names only |
import "math" / import "io" |
Import a built-in stdlib module |
fn / struct in a module |
Automatically available to importers |
print(...) in a module |
Does NOT execute during import |
| Relative paths | Resolved from the importing file's directory |
| Built-in module names | Checked before file lookup ("math", "io") |
| Circular imports | Detected and reported as an error |
| Caching | Each module is compiled at most once |