Skip to content

Modules And Visibility

This chapter documents Zynx module identity, imports, package lookup, exported interfaces, privacy, prelude bindings, declarations, and attributes.

Status: current public API unless a rule is marked unsupported or unspecified.

Modules And Imports

The module identity is a dotted module name. Explicit module declarations are rejected.

For the entry source file, the module name is derived from the source path. A normal name.zx file becomes module name. Standard-library files under lib/std/ become std.*; lib/std/builtin.zx is module builtin, and init.zx is stripped so lib/std/collections/init.zx is std.collections.

For a named import, the requested import path is the module identity. For example, import std.io; loads the source or package selected for module std.io; a package .zxm must advertise the same module name in its manifest. There is no built-in native require("path") operation; import plus declared packages is the module-loading model.

require is an ordinary identifier. It is not a keyword, reserved name, contextual intrinsic, or removed-feature tombstone, so code may use it for a function, parameter, local, field, method, or other declaration:

Zynx
fn require(value: i32) -> i32 {
	return value;
}

fn use(require: i32) -> i32 {
	let result = require;
	return result;
}

A call to a user-declared require(...) uses ordinary name resolution. An unbound require(...) receives the same unresolved-name diagnostic as any other unbound call; it does not select a native loader or produce a dedicated loader diagnostic.

Named module resolution is deterministic within the configured inputs:

  • The importing file's directory is searched first.
  • Configured module search paths are searched after the local directory.
  • Within each search root, source .zx candidates are preferred over .zxm bundles.
  • name.zx, name/init.zx, a single source file inside name/, and target-specific subdirectories are supported candidate forms.
  • This precedence means an in-tree local source module intentionally wins over an installed package with the same module name.

A non-standard module identity may resolve to at most one configured search-path candidate. If two explicit, project, or package search roots provide the same non-standard module name, the import is ambiguous and rejected with both candidate paths. The importing file's directory is checked before that ambiguity rule, so local source next to the importer may deliberately shadow packages. builtin and std.* are reserved for the active standard library/SDK.

Packages, Bundles, And Lockfiles

The package distribution contract is in the Package Reference. Package distribution uses exact source and exact git dependencies only.

Zynx has two build/artifact manifest formats plus one deliberately separate deployment launch document:

  • zynx.json is the source project manifest. It must use "format": "zynx-project" and "version": 1, declare package.name, may declare package.version, declares build targets under targets, selects default_target, and may declare dependencies under dependencies.
  • A .zxm bundle contains an embedded bundle manifest with "format": "zxm", "version": 1, module, target, abi, and kind. ZXM remains at version 1 while the language and package format are unreleased. After the first public language release, every incompatible on-disk format change must increment this version. The bundle module must exactly match the imported module identity. The bundle target and ABI must match the active compiler target. Named imports accept source, interface, or static bundles only. Dynamic bundles and runtime module loading are unsupported.
  • zynx-launch version 1 is a deployment-only per-instance grant document supplied explicitly through zynx run/launch --grants. It binds the exact executable-root @host requirement digest to typed grants. It is never an import, package/build input, dependency, lockfile entry, resource, argv/env value, or artifact semantic-identity input. There is no auto-discovery, include, overlay, override, or merge path. Every concrete grant has one mandatory closed kind-specific policy; an explicit empty policy denies all operations, while omission is invalid. The canonical schema is documented in Host Capability Requirements. RFC 0070 keeps version 1 frozen and adds only five directory namespace rights through the complete version-2 schema; Network policy is unchanged.

Dependency entries are exact git dependencies. The dependency key is the package name used by the project, the dependency shape is { "git": "...", "rev": "..."? }, and semver range solving and registry selection are unsupported. package.version is package metadata; it does not participate in dependency selection. Dependency entries may not contain other keys; unsupported keys such as version, range, registry, mirror, or signature are rejected.

zynx.lock is the dependency selection input for reproducible builds. It must use "format": "zynx-lock" and "version": 1, and its packages object maps dependency names to exact git, rev, and cache path values. Resolution rules are:

  • A manifest dependency with rev is pinned to that exact revision.
  • Without a manifest rev, an existing lock entry for the same dependency name and git URL is reused.
  • If no matching lock entry exists, or zynx update [package] is requested, the resolver selects the git remote HEAD and writes the new exact revision to zynx.lock.
  • A locked build must not silently advance dependency revisions. It may fetch a missing locked checkout into the package cache; fully offline builds require the lockfile, package cache, and SDK/stdlib inputs to already be present.

Builds are hermetic with respect to declared import declarations, the project manifest, zynx.lock, the configured package cache, explicit module paths, and the active SDK/stdlib. Source cannot load undeclared modules at runtime to bypass locked package resolution.

Embedded Package Resources

@embed("name") takes one literal logical name from the resources map of the package that defines the current module. It does not take a source-relative or working-directory-relative path. Resolution never consults the importing package or searches ambient directories.

json
{
  "resources": {
    "wire/golden-request": "fixtures/request.bin"
  }
}
Zynx
fn check_request() {
    let request: [u8] = @embed("wire/golden-request");
    _ = request;
}

The build validates package-root-relative file confinement, snapshots exact bytes before compilation, and records their content digests as build and artifact inputs. The compiler resolves the expression to immutable program-lifetime bytes and targets read-only native data or a core-Wasm data segment. The builtin is not legal in a const-evaluated context. Using @embed never grants a Wasm filesystem capability.

An undeclared or nonliteral name is a hard error. A path-looking string remains only a logical-name lookup and has no legacy path fallback. See Embedded Resources for manifest validation, result lifetime, and migration rules.

For a project build, module lookup uses this order:

  1. the importing file's local directory;
  2. explicit --module-path roots;
  3. locked dependency package roots, including built module outputs when present and dependency source roots;
  4. the active SDK/stdlib roots for builtin and std.*.

Duplicate non-standard module identities across step 2 or step 3 are rejected instead of being resolved by path order. A local module from step 1 may still shadow those packages intentionally.

The static import graph must be acyclic. If loading an import would re-enter a module already on the active load stack, the import cycle is rejected and the cycle path is reported, for example module import cycle: a -> b -> a.

A module path with an internal component is internal to the dotted parent before that component. Only modules inside that parent may import it. For example, std.fs.internal.dir may be imported by std.fs and std.fs.* implementation modules, but not by ordinary user modules; std.fs.internal.errors is internal to std.fs.

Imports create bindings:

Zynx
import std.time;                  // binds `time`, and the dotted `std.time` path
import std.time as clock;         // binds only `clock`
import std.time.{ ns };           // binds `ns`
import std.time.{ ns as nanos };  // binds only `nanos`

Module imports bind a namespace value. Unaliased dotted imports bind the final path component and also make the dotted path usable for module-qualified member access. Aliased imports bind only the alias; the final path component is not also introduced. Neither form injects the module's members as short names. Member imports bind exported members directly. When a member import uses as, only the alias is introduced.

Imported declarations must be exported by the imported module. Import aliases participate in the ordinary top-level duplicate-symbol and reserved-name rules.

Namespace And Selective Resolution

import mod; is namespace-only. Use mod.Symbol, or write import mod.{ Symbol }; when the short name is intentional. There is no * form, implicit wildcard candidate set, lazy wildcard ambiguity, or wildcard shadowing rule.

Zynx
import std.async;
import std.async.{ Future };

fn qualified() -> ^async.Future<i32> {
	return 7;
}

fn short() -> ^Future<i32> {
	return 42;
}

If two selective imports request the same local name, ordinary duplicate-name checking applies immediately. Use as or keep one use qualified. Function overloads imported together from one selective source retain their canonical overload set.

Visibility And Privacy

Each source file is a module. Top-level declarations are module-private by default, even when they are in a package dependency. Package boundaries affect module resolution and reproducibility, not visibility.

A top-level declaration becomes part of the module interface only when it is marked with export or introduced by a selective re-export. Re-exported declarations must already be exported by their original module. The only form is:

Zynx
export import math.{ sum, product as multiply };

export import math; and export import math as m; are errors. Module namespaces are never re-exported. A caller imports a dotted child module directly when it needs that namespace. extern and intrinsic do not export a declaration by themselves.

Module imports expose only the imported module namespace. Member lookup through that namespace can resolve only exported declarations:

Zynx
import math;
import math as m;

Member imports bind exported declarations directly:

Zynx
import math.{ sum };
import math.{ sum as add_all };
export import math.{ sum as public_sum };

Aliases only change the local binding name. They do not bypass module privacy, and they do not introduce the original name when an alias is present. A re-export is a public binding to the canonical origin declaration. It creates no new type, function, value, wrapper, or ABI symbol.

Bindings with the same public name and canonical origin DefId are idempotent; identical canonical overload sets coalesce in the same way. A different target under that name is an eager interface error with an as suggestion. A local declaration and re-export cannot share a name. Explicit transitive re-exports are allowed, but alias-only dependency cycles are rejected.

Every field, method, nested type, associated item, and variant payload field is private by default, even when its containing type is exported. Add export to each member intended for external access. Exporting a container does not recursively export its members. There is no @private; old declarations remove that attribute and add export only where public access is intended.

Interfaces can be used as generic constraints and, when object-safe, as dynamic interface values. An exported interface can be named by importing modules; a non-exported interface cannot be imported or used across the module boundary. Default methods on interfaces follow the same exported-interface visibility rule.

Semantic artifact interfaces expose the exported module interface. A selective re-export records public_name -> canonical origin DefId plus the origin dependency digest. It does not copy the declaration. Non-exported dependency declarations required to type-check an exported signature, default expression, or generic body remain implementation dependencies and never become public bindings. Non-exported helpers and unused private types are omitted.

Direct, aliased, and diamond-qualified paths to one origin have identical nominal identity. Native linkage emits the origin definition exactly once and does not manufacture facade wrappers or alias symbols. Go-to-definition follows a re-export to the origin declaration; completion may still present the public alias written by the facade.

Visibility diagnostics must be direct: imports of non-exported members report that the module has no exported member, module-qualified access reports that the module has no member, and private field or method access reports the private member by name. A re-export conflict names both origin definitions and suggests aliasing one with as.

The prelude is the set of exported declarations from module builtin plus the root std namespace that are available without an explicit import in ordinary modules. The builtin set is Copy, Scalar, Integer, ByteAlignedInteger, Signed, Unsigned, and Float. The builtin module namespace is also available as builtin. The root std namespace exposes only selected daily-use helpers: std.copy, std.move, std.drop, and std.iter. It does not auto-import stdlib submodules; std.net, std.json, std.io, and other real modules still require explicit imports before use. Keywords, built-in type tokens, and unique are language names, not prelude imports. require remains an ordinary identifier as described above. Canonical Copy is the sole protected compiler-known duplication marker. Copyable and ImplicitCopy are not exported legacy markers or reserved names; user declarations and imports with those names resolve normally. An unresolved Copyable bound may suggest Copy, but a visible user symbol is never rewritten or blocked.

Top-level declarations include:

  • import declarations, including import module.{ ... } member-import forms
  • selective export import module.{ ... } declarations; namespace re-exports are not declarations
  • fn and unsafe fn
  • struct, union, enum, error, interface, and type
  • test
  • top-level let and const; per-thread mutable state is represented by a module-level let key = ThreadLocal<T>(factory) descriptor (a plain mutable var global is rejected in safe code)
  • root-private @host let name: fs.Dir | net.Network | mmio.Region in _; requirements in the selected executable composition root; these are target bindings rather than exported globals

export, extern, and intrinsic can be written as declaration modifiers. Plain extern fn and extern let declare external Zynx-linkage symbols. extern "C" and extern library are the explicit declaration forms for non-Zynx ABI boundaries. Attribute syntax is @name or @name(args...). Attribute names are identifiers. export, extern, and intrinsic have no attribute spelling: they are keyword modifiers only, and @export, @extern, and @intrinsic are parse errors. Declaration-prefix attributes must appear before export/extern/intrinsic modifiers. Struct field attributes may also be written as suffix attributes after the field type and before the default value, for example var field: i32 @readonly = 1. asm is only a statement form inside a function body and requires an unsafe context; top-level asm is not supported.

The attribute table is closed. Unknown source attributes are rejected, and user-defined decorators are unsupported. Names beginning with __zynx_ are reserved for compiler-private generated code and are not a documented user extension mechanism. Attribute order has no semantic meaning except that attributes must appear before declaration modifiers. Duplicate attributes are rejected except repeated @suppress(...).

AttributeAllowed targetsArgumentsEffect
@hostAn uninitialized typed module-level let in the selected executable composition rootnoneDeclares one required private per-instance shared loan of exactly std.fs.Dir, std.net.Network, or std.mmio.Region. Only the direct body of parameterless main may resolve it; it cannot be exported, imported, re-exported, or declared by a dependency.
exportTop-level functions, structs, enums, errors, interfaces, type aliases, globals, and extern ABI declarationsnoneExports the declaration from the module interface.
externTop-level functions and globalsnoneDeclares a Zynx-linkage symbol whose body is provided by another object or module unit. When extern is followed by an ABI string or library, it starts an extern ABI declaration instead.
extern "ABI" / extern libraryTop-level functions and globals; extern library blocksABI string; optional library block with abi / link directivesDeclares a non-Zynx ABI symbol. Supported forms include extern "C" fn name(...); and extern library "zlib" { abi "C"; link "z"; ... }.
@symbolextern ABI declarationsone string literalUses the named ABI symbol while keeping the Zynx declaration name local.
intrinsicFunctions and methodsnoneMarks an intrinsic declaration. Normal source cannot define new intrinsic behavior with this attribute.
@inlineFunctions and methodsnoneInline hint. Rejected with external declarations.
@discardableFunctions and methodsnoneSuppresses ignored-result warnings for calls.
@deprecatedFunctions and methodsoptional string literalEmits a deprecation warning on calls, optionally with the message.
@weakFunctions and methodsnoneEmits weak linkage for generated definitions.
@sectionFunctions and methodsone string literalPlaces the generated function in the named object section.
@noreturnFunctions and methods returning never; body-less external declarations may return voidnoneMarks the generated function as not returning to its caller.
@unmangledFunctions, methods, and globalsnoneUses the source name as the emitted symbol name.
@packedStructs and extern-library unionsnoneUses packed source-order layout. Rejected with @strict.
@strictStructs and extern-library unionsnoneUses source-order layout with target ABI alignment and padding. Rejected with @packed.
@c_nameStructs, unions, enums, errors, and globalsone string literalRecords the C spelling used by bindgen/header emission.
@readonlyStruct fieldsnoneRestricts post-construction writes to a var field to methods of its declaring struct; it does not replace the required var/let. A let field remains immutable to everyone.
@bitsFields of C ABI structs in extern library blocksone integer literalDeclares a C bitfield width.
@suppressLocals and declarations that document supported warningsone or more warning namesSuppresses the named warning kind for that declaration. It cannot suppress hard attribute errors.
@unusedLocal bindingsnoneMarks the binding as intentionally unused.
@abi, @arch, @platformTop-level declarations and methodsone string literalTarget filters; a declaration is present only when every filter matches the current build target.
@target_featureFunctions and methods with source bodies, except mainone or more std.cpu.Feature valuesRequires and enables the normalized feature set for this function. Strings, +/-, duplicates, wrong-target variants, extern declarations, and coercion to callable values are rejected.

For Zynx functions or methods with source bodies, @noreturn requires a declared never return type. Body-less external or extern ABI declarations may use @noreturn with void when the imported ABI symbol never returns.

@suppress accepts bare warning names, string literal warning names, or named arguments whose value is a warning name, such as @suppress(unused) or @suppress(kind: unused). The currently recognized warning names are unused, deprecated, unknown_attribute, unused_import, heap_efficiency, stack_size, unnecessary_cast, unnecessary_return_type, unnecessary_return, and init_checker_limit. Unknown warning names are errors. Despite the unknown_attribute name, unknown source attributes remain hard errors.

unsafe is a function modifier. Duplicate modifiers are rejected. unsafe fn means calls to the function require an unsafe context; it does not make the function body implicitly unsafe. The former async fn spelling is rejected; write an explicit Future<T> return type instead.

Released under the MIT License.