Skip to content

Module System

Shape modules are loaded through imports and project/script configuration.

For the target scoping model, see Names and Scope.

With the scoped import model, user-facing names come into scope only through use / from ... use or the tiny implicit prelude. Importing a module namespace does not inject its functions, types, or annotations as bare names.

use std::core::state
let frame = state::capture()
use std::core::state as s
let frame = s::capture()
from std::core::snapshot use { Snapshot }
from math::stats use { mean as avg }
from std::core::remote use { @remote }
@remote("worker:9527")
fn process(data) { data }
use std::core::remote
@remote::remote("worker:9527")
fn process(data) { data }

Module paths use :: separators:

from mylib::transforms::normalize use { normalize }

Namespace calls use :: as well:

use mylib::transforms::normalize as normalize
let value = normalize::run(input)

Imports resolve in this order:

  1. Extension-provided modules (highest priority)
  2. Bundle-provided modules (from .shapec dependencies)
  3. Embedded standard library (std::*)
  4. Filesystem: project root, dependency paths, [modules].paths, stdlib path
math_utils.shape
pub fn clamp(x: int, lo: int, hi: int) -> int {
if x < lo { lo } else if x > hi { hi } else { x }
}
pub type Bounds {
lo: int,
hi: int
}
pub annotation traced(tag) {
targets: [function]
}

Then:

from math_utils use { clamp, Bounds, @traced }

Shape maps the filesystem to module paths directly. A file at math/linalg.shape is importable as math::linalg. A file at transforms/normalize.shape becomes transforms::normalize.

myproject/
├── shape.toml
├── main.shape
└── math/
├── linalg.shape → import as "math::linalg"
└── stats.shape → import as "math::stats"

To add extra search roots beyond the project directory, configure [modules] in shape.toml:

[modules]
paths = [
"./vendor", # local vendor directory
"/usr/local/shape", # system-wide modules
]

With this config, vendor/mylib/utils.shape is importable as mylib::utils.

A minimal library with two files that import each other:

math/linalg.shape
from std::core::intrinsics use { Vec }
from math::stats use { mean }
pub fn normalize(v: Vec<number>) -> Vec<number> {
let mu = mean(v)
v.map(|x| x - mu)
}
math/stats.shape
from std::core::intrinsics use { Vec }
pub fn mean(v: Vec<number>) -> number {
v.sum() / v.length
}
pub fn variance(v: Vec<number>) -> number {
let mu = mean(v)
v.map(|x| (x - mu) * (x - mu)).sum() / v.length
}

Then from main.shape:

from math::linalg use { normalize }
from math::stats use { variance }
let data = [10.0, 12.0, 11.5, 13.0, 9.5]
let normed = normalize(data)
let v = variance(data)
print(f"normalized: {normed}")
print(f"variance: {v}")
  • Use from ... use as the default import style in this book.
  • Use use module_path when you want a namespace and module::member(...) call sites.
  • Import annotations explicitly with @ so they remain distinct from ordinary values and types.
  • Keep module APIs typed and small.
  • Extensions expose modules through the same runtime module loader path.