Zenth supports splitting programs across multiple files using local modules. A module is simply a .zn File (or a directory of .zn files) that you import into your program.

#Importing a Local Module

Use a path starting with ./ or ../ to import a local module:

import "./utils";
import "./math/geometry";
import "../shared/helpers";

The module name is derived from the filename stem (the last path component without .zn). Use as to give it a custom alias:

import "./mathutils" as math;
import "../geometry/shapes" as shapes;

#Using Module Functions

Call functions and access types from a module through its name (or alias):

import "./mathutils";

fn main() {
    let result = mathutils.add(3, 4);
    Println(Str(result));
}

With an alias:

import "./mathutils" as math;

fn main() {
    let result = math.add(3, 4);
    Println(Str(result));
}

#Module Files

A module file is a regular .zn file that does not define fn main. It can contain functions, objects, constants, and type aliases, all of which become available to any file that imports it.

// mathutils.zn
const pi = 3.14159;

fn add(a: Int, b: Int) -> Int {
    return a + b;
}

fn multiply(a: Int, b: Int) -> Int {
    return a * b;
}

Rules for module files:

  • Must not define fn main
  • Can import other local modules
  • Can define functions, objects, constants, and type aliases
  • All top-level definitions are accessible to importers

#Using Module Objects

Objects defined in a module are available to importers. Construct them using module.ObjName(field=value) syntax:

// shapes/rect.zn
obj Rectangle {
    width: Float;
    height: Float;

    fn area() -> Float {
        return self.width * self.height;
    }
}
// main.zn
import "./shapes";

fn main() {
    let r = shapes.Rectangle(width=10.0, height=5.0);
    Println(Str(r.area()));  // "50"
}

#Directory Modules

If the import path points to a directory, Zenth loads all .zn files in that directory as a single module. The module name is the directory name:

project/
  main.zn
  geometry/
    rect.zn      ← contains Rectangle obj
    circle.zn    ← contains circle_area fn
// main.zn
import "./geometry";

fn main() {
    let r = geometry.Rectangle(width=3.0, height=4.0);
    Println(Str(r.area()));

    let a = geometry.circle_area(5.0);
    Println(Str(a));
}

All definitions across the files in the directory are merged into one module namespace.

Zenth resolves module paths relative to the file containing the import:

  • import "./utils" → looks for utils.zn in the same directory, or a utils/ directory
  • import "../shared" → looks one directory up

#Example: Multi-file Project

project/
  main.zn
  testmods/
    mathutils.zn
    shapes/
      rect.zn
      circle.zn
// testmods/mathutils.zn
fn add(a: Int, b: Int) -> Int {
    return a + b;
}

fn multiply(a: Int, b: Int) -> Int {
    return a * b;
}
// main.zn
import "./testmods/mathutils" as math;
import "./testmods/shapes";

fn main() {
    Println(Str(math.add(3, 4)));         // "7"
    Println(Str(math.multiply(6, 7)));    // "42"

    let r = shapes.Rectangle(width=3.0, height=4.0);
    Println(Str(r.area()));               // "12"
    Println(Str(shapes.circle_area(1.0))); // "3.14159"
}

#Compared to Standard Library Imports

Standard libraryLocal module
Path"fmt", "math", etc."./path/to/module"
SourceGo stdlibYour .zn files
Aliasimport "math" as m;import "./utils" as u;

Both use the same module.function() call syntax.

#Module-Internal Function Calls

Functions within a module can call other functions defined in the same module. This allows you to build composable modules where functions delegate to each other:

// mathutils.zn
fn add(a: Int, b: Int) -> Int {
    return a + b;
}

fn multiply(a: Int, b: Int) -> Int {
    return a * b;
}

// sum_and_double calls add and multiply within the same module
fn sum_and_double(a: Int, b: Int) -> Int {
    let s = add(a, b);
    return multiply(s, 2);
}
// main.zn
import "./mathutils";

fn main() {
    Println(Str(mathutils.sum_and_double(5, 3)));  // "16"
}

#Qualified Types

When you need to refer to a type defined in a module, use module.Type syntax. This works in type annotations, function parameters, and generic type positions like Array() and Tuple():

// items.zn
obj Item {
    name: Str;
    value: Int;
}

fn make_item(name: Str, value: Int) -> Item {
    return Item(name=name, value=value);
}
// main.zn
import "./items";

fn print_items(data: Array(items.Item)) {
    for item in data {
        Println("{item.name}: {Str(item.value)}");
    }
}

fn main() {
    let a = items.make_item("apple", 3);
    let b = items.make_item("banana", 5);
    let list: Array(items.Item) = [a, b];
    print_items(list);
}

Qualified types work anywhere a type is expected:

  • Function parameters: fn process(item: task.Task) { ... }
  • Variable annotations: let items: Array(task.Task) = [];
  • Generic positions: Array(items.Item), Tuple(Int, task.Task), Hashmap(Str, task.Task)