Skip to content

Modules

A Pluvial program can be split across multiple .plu files. The module system is entirely compile-time name resolution — there are no new bytecode operations. Each file is a module; names are private unless exported.

Prefix a declaration with export to make it visible to other files. Anything not exported is module-private:

export def add(int a, int b) int { return a + b }
export struct Point { int x int y }
export auto VERSION = "1.0" # exported constant (literal initializer)
def helper() int { return 42 } # not exported — module-private

import "path" brings a module in under its name; you then reach its members with Module.name:

import "math" # then math.add(1, 2)
import "./utils/strings" as s # rename with `as`, then s.repeat("ab", 3)

Module.name works for functions, structs, and in type positions.

from "path" import ... binds names directly into the importing file’s scope:

from "math" import add, mul # add(1, 2) / mul(3, 4)
from "math" import add as plus # plus(1, 2)
from "shapes" import Point, origin # structs and functions together

Both forms can coexist in one file: after import "math", writing add(1, 2) uses a from-binding while math.add(1, 2) uses qualified access.

  • The module key is the absolute path (via realpath), so ./math.plu, math.plu, and ./../foo/math.plu resolve to the same module if they point to the same file. A module’s top-level code runs once, Python-style.
  • The module name is the last path component without .plu (./utils/stringsstrings), and as can rename it (form 1).
  • Relative paths are resolved against the importing file’s directory; absolute paths (starting with /) are used as-is.
  • Modules execute in topological order — dependencies run their top-level code before the modules that import them.
  • Circular imports are detected (a → b → a) and reported as an error.

A Module.name referring to a private (non-exported) name reports 'X' is not exported from module 'Y'; an unknown name reports module 'Y' has no export 'X'.

Collisions between an import and a local definition are detected in source order: defining add and then from "math" import add (or the reverse) is a compile error.

The std/ prefix is reserved for the standard library:

import "std/strings"

std/<name> is resolved by trying, in order: the PLUVIAL_STD_PATH environment variable (if set), the system install path, and ./stdlib (relative to the working directory). Each candidate has <name>.plu appended and is resolved with realpath; the first hit is canonical. If none resolve, the import fails with a “standard library module ‘std/X’ is not yet available” error.

Standard-library imports have a dot-style shorthand equivalent to the quoted form:

Quoted formShorthand
import "std/math"import std.math
import "std/math" as mimport std.math as m
from "std/io" import read_filefrom std.io import read_file
import std.foo.bar → resolves to std/foo/bar.plu

Rules:

  • The leading identifier must be stdimport foo.bar (unquoted, non-std) is a parse error.
  • Multi-segment names rewrite simply: std.foo.bar"std/foo/bar".
  • Relative paths still require quotes (import "./utils").
  • as works the same way in both syntaxes.

See the Standard library overview for the available modules.