Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modules

A Bop program can be split across multiple files — or in-memory source strings, asset bundles, anywhere the embedding host can return Bop source. The use statement pulls another module’s public surface into the current scope.

The four forms of use

use path                    // glob:        everything public
use path.{a, b, Type}       // selective:   just the listed items
use path as m               // aliased:     binds `m` as a Value::Module
use path.{a, b} as m        // aliased + selective

Paths are dot-joined identifiers: std.math, game.entity.player. How the host resolves a path is up to the embedder — bop-sys’s StandardHost::with_module_root maps foo.bar to <root>/foo/bar.bop, in-memory hosts can look up a string table, a web host can fetch a URL. See Embedding.

Glob use

Brings every public export of a module into the current scope as a bare name:

use std.math
print(PI)            // constant from std.math
print(factorial(5))  // fn from std.math → 120

Names that start with _ are considered private by convention and glob imports skip them:

// In module `foo`:
fn _helper() { return 42 }
fn public() { return _helper() }

// Elsewhere:
use foo
print(public())      // 42
// print(_helper())  // error: `_helper` not in scope

Glob is idempotent at the injection site — running use foo twice in the same scope is a no-op (matches Python’s import foo; import foo). When two glob imports would introduce the same name, the first wins and the second emits a runtime warning — explicit selective imports are the way to disambiguate.

Selective use

Pick exactly which names you want:

use std.math.{PI, factorial}
print(PI)
print(factorial(4))
// print(clamp(1, 0, 10))   // error — not imported

Selective imports can reach private names explicitly:

use foo.{_helper}
print(_helper())     // ok — explicit opt-in

If a listed name doesn’t exist in the target module, you get a clear error pointing at the use site.

Aliased use

Binds the whole module as a single value under the alias:

use std.math as m
print(m.PI)
print(m.factorial(5))

m is a Value::Modulem.type() is "module". You access its exports via the . operator. Methods on aliased modules (m.helper(...)) work the same way they would on a bare imported fn.

Combine with selective to shrink the alias’s surface:

use std.math.{PI, factorial} as m
print(m.PI)
print(m.factorial(5))
// print(m.clamp(1, 0, 10))   // error — `clamp` wasn't imported

Namespaced types

User-defined struct and enum types can be constructed and pattern-matched through the alias:

// In `paint.bop`:
enum Color { Red, Green, Blue }
struct Point { x, y }

// In main:
use paint as p
let c = p.Color::Red
let origin = p.Point { x: 0, y: 0 }

print(match c {
  p.Color::Red   => "stop",
  p.Color::Green => "go",
  p.Color::Blue  => "cool",
})

The namespace is required — bare Color::Red inside the main file wouldn’t find the type unless you also imported paint.{Color} by bare name.

Type identity

Types carry their declaring module as part of their identity. Two modules can declare a type with the same name; values from them are distinct types — equality is always false across the module boundary, and patterns only match values from the module the pattern named.

// paint.bop: enum Color { Red, Blue }
// other.bop: enum Color { Red, Green, Yellow }

use paint as p
use other as o

let a = p.Color::Red
let b = o.Color::Red

print(a == b)        // false — different `Color` types
print(a == a)        // true

A pattern over an aliased module’s type only fires for values from that module:

fn label(c) {
  return match c {
    p.Color::Red => "paint-red",
    o.Color::Red => "other-red",
    _            => "something else",
  }
}
print(label(p.Color::Red))   // "paint-red"
print(label(o.Color::Red))   // "other-red"

This is Bop’s answer to the “same-named type, different shape, in different modules” problem. No renames required.

Re-exports are transitive

A module’s effective exports include everything it uses from other modules (minus privacy filtering). If a does use b and b declares fn foo(), then use a in the top-level program makes foo visible too. The same applies to types — importing a brings b’s public types in scope.

Builtin types

Result and RuntimeError are engine built-ins. They’re always in scope — you don’t need any use to write Result::Ok(v) or to match on RuntimeError { message, line }. The combinators (unwrap, map, and_then, …) live as methods on the Result type, also always available. See Error Handling.

Cycles

Circular imports (a uses b which uses a) are detected at load time and raise a clear error naming the cycle path. Restructure the code so the cycle breaks — usually by pulling shared definitions into a third module that neither circular node depends on.

Inside a function body

Aliased modules and bare-imported types remain visible inside function bodies declared in the same module:

use paint as p

fn describe(c) {
  return match c {
    p.Color::Red   => "red",
    p.Color::Blue  => "blue",
    _              => "other",
  }
}

The p alias doesn’t need to be a parameter — module-level aliases persist across function call boundaries so patterns inside fn bodies can resolve them.