Documentation menu
Language

Modules

Every .sz file is a module. export marks what other files may use, and import pulls those exports into the current scope.

export and import

Prefix a function with export to make it reachable from another file:

// src/math.sz
export fn int double(int n) { return n * 2 }
export fn int quadruple(int n) { return double(double(n)) }

Then import the module by path, without the extension:

// index.sz
import "src/math"

out double(5)      // → 10
out quadruple(5)   // → 20

Imported names land directly in the importing scope — there is no namespace object and no as alias. import "src/math" loads src/math.sz.

Paths are relative to the importing file

Import paths resolve against the directory of the file doing the importing, not the project root. This is the usual stumbling block in a package with index.sz at the root and modules under src/:

// index.sz       →  import "src/parser"    ✅
// src/lexer.sz   →  import "parser"        ✅  sibling, simple name
// src/lexer.sz   →  import "src/parser"    ❌  looks for src/src/parser.sz

The wrong form often passes your own tests — those run with the repo as the working directory, which happens to resolve it — and only fails once someone consumes the package by name.

Importing a package by name

A bare name is looked up across several roots in order: the app's directory, the current working directory, <cwd>/packages, SEREZ_HOME, the directory of the sz executable, and ~/.serez/packages — trying <root>/<pkg>/index.sz at each:

import "serez-ui"

Export visibility rules

Visibility is decided per module, and it is all-or-nothing:

  • A module that uses export at least once exposes only what it explicitly exported. Everything else it declared at its own top level is removed when it finishes loading.
  • A module that uses export nowhere exposes everything it declared at the top level (backward-compatibility rule).

export wraps declarations: export let, export fn, export class, export interface, and export enum.

Exports leak transitively

Cleanup only inspects declarations from that specific module. If module A imports module B, anything B exported remains in the global runtime environment. Consequently, importing A makes B's exported symbols visible in your file as well. Treat the exported symbols of reachable dependencies as part of the shared namespace.

Flat namespace & collisions

There is a single flat namespace for runtime symbols. If an imported module declares a name that was already defined, the module's definition overwrites it, and the compiler emits a diagnostic warning:

⚠️  WARNING: importing '.../collide.sz' replaced 'collide', which this file
    already defined. The module's definition wins from here on.

Modules are canonicalized and executed only once; duplicate imports of the same file are no-ops. Recursive/cyclic imports terminate safely.

Top-level constraint

import statements belong at the top level of a file. If placed inside a function or block, functions and let bindings evaporate when that local frame exits, leaving only registered types behind.