Skip to content

Modules and exports

Every .zs file is a module. Declarations are private unless marked export, and an export-only module does not need function main().

geometry.zs
export struct Point {
x: i32;
y: i32;
}
export function total(in point: Point): i32 {
return point.x + point.y;
}
function internalAudit(in point: Point): void {
// private to this module
}

internal exposes implementation declarations to sibling modules in the same manifest-defined package without publishing them to consumers:

src/window-state.zs
internal struct WindowRecord {
id: String;
}
export class WindowManager {
internal function publish(inout this, in record: WindowRecord): void {}
}

Sibling modules use ordinary relative imports. A dependent package cannot import WindowRecord or call publish, and those names are omitted from its completion and public metadata. The owning z.json must declare package.name; directory proximity or presence in one linked graph does not grant access. internal and export are mutually exclusive.

The same boundary applies to internal constructor: dependent packages may name and receive the class, but only the owning package may construct it. The inaccessible constructor signature is omitted from public metadata and editor call surfaces.

internal is a Z package boundary, not a C visibility annotation or a native-code security boundary. An internal function cannot also be export c; only explicit export c function declarations become supported C entry points and appear in generated consumer headers. Public C wrappers may call internal Z operations, preserving a narrow external ABI. A C-layout type that appears in that ABI must still be described in the header even when dependent Z packages cannot import its source declaration.

Generated framework code follows the same package boundary. A versioned .zbuild.json overlay may assign a generated module an existing package identity, allowing that generated module to use the package’s internal API. A checked call adapter may carry the identity only to its synthetic replacement member access. It does not make the member public, grant access to authored application code, expose it through completion or metadata, or bypass private; omitting the package identity makes the generated access external.

A namespace import keeps the source module visible at use sites:

import geometry from "./geometry.zs";
const point = geometry.Point({ x: 20, y: 22 });
const answer = geometry.total(point);

This form is useful when a module exports several related names or when qualification makes code easier to read.

Import only the declarations a module needs:

import { Point, total } from "./geometry.zs";
import { translate as movePoint } from "./geometry.zs";

Aliases are ordinary local module bindings. Hover and definition navigation should show both the local alias and original exported declaration.

Z ownership words are contextual, so an imported declaration may still use a name such as move when the surrounding grammar is unambiguous:

import { translate as move } from "./geometry.zs";
const shifted = move(point, 1, -1);

Modules may publish immutable compile-time values:

app-config.zs
export const APP_NAME: String = "Z Notes";
export const DEFAULT_PORT: u16 = 34115;
export const ENABLE_METRICS: boolean = true;
const CONFIG_REVISION = 1;

An exported or internal constant requires an explicit type so its cross-module contract cannot change when the initializer is edited. A module-private constant may infer its type.

Module storage is immutable: there is module-level const, not module-level let. Importing a Z module does not execute a hidden initializer. Runtime application roots use explicit ownership mechanisms such as Once<T> rather than depending on initialization order.

The current executable constant tier accepts cleanup-free literals and owned literal String values. General constant evaluation and ownership-bearing runtime globals remain separate future work.

Use std/embed when immutable file bytes should ship inside the native binary:

import embed from "std/embed";
export const INDEX_HTML: embed.StaticBytes =
embed.bytes("./assets/index.html");

The path must be a string literal and is resolved relative to its declaring module. Missing files and directories are compile errors. embed.StaticBytes is immutable process-lifetime storage with .length, checked indexing, and implicit immutable Slice<u8> borrowing. It is cleanup-free, copyable, and thread-shareable; runtime Arrays and Slices cannot be converted into it.

The same source shape names three kinds of module:

import console from "std/console"; // standard library
import geometry from "./geometry.zs"; // Z source
import sqlite from "sqlite3.h"; // native header

Resolution is not the same behind the scenes:

  • Relative .zs paths join the checked Z source graph.
  • std/... resolves a standard-library module.
  • Header imports ask Clang for native ABI declarations and layouts.

Source libraries declare their package identity and public entry points in z.json:

{
"package": {
"name": "zapp",
"version": "0.1.0"
},
"library": {
"entry": "src/zapp.zs",
"exports": {
"./window": "src/window.zs"
}
}
}

The consumer declares one dependency alongside its target:

{
"dependencies": {
"zapp": { "path": "../zapp" }
},
"target": {
"name": "notes",
"entry": "main.zs"
}
}

The compiler and editor then resolve import { Application } from "zapp"; without source rewriting. The library entry supplies the package root and only exact exported subpaths are public. Local path dependencies work transitively in Stage 0 and the native driver; registry versions, downloads, and lockfiles remain future layers.

Systems packages can own their native implementation requirements alongside their public source surface:

{
"package": { "name": "desktop", "version": "0.1.0" },
"library": {
"entry": "src/desktop.zs",
"native": {
"includeDirectories": ["include"],
"link": {
"directories": ["lib"],
"libraries": ["desktop_native"],
"frameworks": ["AppKit"]
}
}
}
}

Those paths resolve from the package’s own manifest and compose transitively with the consumer target. Applications select deployment policy; they do not repeat a dependency’s headers, native libraries, or frameworks. Package-native requirements are unconditional in the first tier; platform clauses follow the language’s target/comptime design later.

A native import does not introduce an automatic wrapper call. See Native interop for header and .zd behavior.

Z resolves an acyclic source-module graph and reports missing modules, duplicate bindings, unavailable exports, and cycles directly. It does not use a separate import type syntax to paper over runtime module cycles: Z imports do not execute JavaScript-style module initialization, and accepted source graphs remain statically resolvable.

Project entry points, artifact kinds, native include paths, libraries, frameworks, and target settings belong in z.json. Application-specific facts live under target; reusable implementation requirements live under library.native. Source modules stay focused on declarations and imports.

See the complete runnable module example.

Next: Closures and function values.