Zax namespaces and modules
| Field | Value |
|---|---|
| Status | Current conceptual design |
| Audience | Human developers organizing declarations or using code across Zax modules |
| Applies To | Programmer-facing namespaces, module roots, source order, imports, injection, declaration exposure, visibility baseline, and module identity; not a formal grammar or build specification |
| Implementation State | Not established by this repository |
| Owns | Protected module-local roots; language-provided root declarations; namespace declaration, reopening, placement, ownership, and self-name shadow permission; source-file contribution and order; named and direct imports; generative module instances; import injection and exact shared dependencies; direct-versus-imported collision behavior; module-internal default visibility; explicit export and non-transitive import baseline; export directive attachment and ranges; directive state stopping at imports; module cycles; namespace/module diagnostics, costs, and source stability |
| Does Not Own | General lexical declaration lookup, aliases, and forwards (declarations and bindings); type-alias identity (identity types); general directive attachment and ranges (compiler directives); exact private and selective-export options; package acquisition and locking; complete global/module initialization; compile-time queries (compile-time code); partial behavior (partials); reflection; or implementation architecture |
Start with a named import
A named import gives another module one stable local root:
Geometry :: import Module.MyGeometryModule
point : Geometry.Point
distance := Geometry.distance(point, origin)
Geometry is the importing source’s name for this imported module instance.
Declarations exported by that instance remain below the root. A subnamespace
keeps the same relationship:
shape : Geometry.Shapes.Polygon
This qualification is more than style. It:
- shows which import owns the declaration;
- avoids collisions with unrelated modules;
- allows several versions or configurations to coexist;
- remains stable when the imported module adds another export; and
- gives diagnostics and tools a durable route to the declaration.
Imports are generative. Importing the same source twice creates two module instances and two sets of declarations:
GeometryA :: import Module.MyGeometryModule
GeometryB :: import Module.MyGeometryModule
first : GeometryA.Point
second : GeometryB.Point = first
// error: GeometryA.Point and GeometryB.Point have distinct identities
Equal source, layout, or injected text does not merge those identities.
compatible posture
may permit an explicit structural relationship, but it never makes two
declarations identical.
Every module has its own root
Every application and imported module instance has one unnamed declaration
root. Module is the protected name for the current instance’s root:
value : Module.Geometry.Point
Inside imported source, Module means that imported module’s root, not the
importer’s root. The importer’s local name does not rewrite it:
ThirdParty :: import Module.ThirdPartyDefinition
Source inside ThirdParty still uses its own Module. There is no
process-wide absolute namespace path.
Module cannot be redeclared, shadowed, replaced by an import, or retargeted by
an alias.
Language-provided declarations
Declarations that are part of Zax itself are present automatically in every module root. An independently developed library is not a built-in and still requires import.
Scalars is a protected exact alias to one language-owned namespace:
Scalars.Integers.I32
Scalars.Fixed.I16F8
Scalars.Floating.Binary32
The same paths are available explicitly through the current root:
Module.Scalars.Integers.I32
Short built-in names such as Integer, I32, and Boolean are protected
language-provided aliases. They do not create another identity and cannot be
shadowed or replaced.
Each generative module instance receives aliases to the same language-owned declarations:
first : GeometryA.Scalars.Integers.I32
second : GeometryB.Scalars.Integers.I32 = first
// Legal: both paths denote the language-owned I32 identity.
This sharing is specific to exact language-provided declarations. It does not
merge declarations written by GeometryA and GeometryB.
Compilation is another protected language-provided name. It answers
compile-time questions about the build, such as Compilation.module.location,
and its answers are specific to the module instance asking. See
compile-time code.
Namespaces organize one module
A namespace is a named declaration scope under the module root or another namespace:
namespace Geometry {
Point :: type {
x : Float
y : Float
}
}
Namespaces use type-like capitalization and ASCII identifier spelling.
Namespace declarations are legal only:
- at module root; or
- directly inside another namespace.
They are not legal inside functions, ordinary blocks, or type bodies. A namespace is a module declaration scope, not a named local lifetime or an implicit type-owned storage mechanism.
Qualified declaration and reopening
Nested and qualified declarations can contribute to the same namespace:
namespace Geometry {
namespace Shapes {
Circle :: type {
}
}
}
namespace Geometry.Shapes {
Polygon :: type {
}
}
For namespace Geometry.Shapes:
Geometryresolves as an ordinary root at that source position.- Every intermediate component must already be a namespace or forwarded namespace.
- Only the final component may be introduced or reopened.
Use Module when the module-root path must be explicit:
namespace Module.Geometry.Shapes {
}
Reopening contributes to one namespace identity. It is not same-scope redeclaration.
Who may reopen a namespace
Ordinary source may reopen a namespace owned by its own module instance. Source cannot extend an imported namespace merely because it can name it:
Library :: import Module.LibraryDefinition
namespace Library.Utilities {
// error: this module instance does not own Library.Utilities
}
An import-injection body is different. Its declarations become source of the target module instance, so it may contribute to a target-owned namespace:
Library :: import Module.LibraryDefinition {
namespace Utilities {
// Target-module contribution.
}
}
An exact namespace alias preserves identity but does not transfer reopening authority.
Adding to an imported type is a different mechanism. A partial may add functions to a type it cannot reopen, and becomes visible only where a scope grants it. Stored members added by a partial reach an imported type only through an injection body, described below.
Self-name shadow permission belongs to one opening
A namespace opening may permit its body to introduce a declaration that hides the namespace’s own unqualified name:
namespace Utilities shadowable {
Utilities :: forward type
// Legal: this opening introduces an incomplete same-named member.
}
namespace Utilities {
Utilities :: type {
}
// Legal completion of the existing forwarded member.
}
Only the opening that introduces the shadowing declaration or forward needs
shadowable. The second opening completes the existing member and does not
repeat the permission.
This modifier is local to the opening:
- it is not a property of the namespace identity;
- later reopenings do not inherit a general shadow grant;
- it does not permit hiding unrelated outer names; and
- it cannot be applied to a protected namespace.
After the nested member exists, Utilities within later namespace source may
name that member. Module.Utilities remains the explicit route to the
namespace itself.
Nothing-policy defaults
The compiler directive:
[<nothing-instance-default=trap>]
selects the omission default for types declared in the attached module or
physical namespace opening. default selects compiler-prepared readable
Nothing behavior.
A namespace setting is local to one opening rather than stored on the namespace identity:
[<nothing-instance-default=trap>]
namespace Services {
First :: type {
}
}
[<nothing-instance-default=default>]
namespace Services {
Second :: type {
}
}
First inherits trap; Second inherits default. The declarations still
belong to the same Services namespace identity.
A nested namespace opening inherits from its physical containing opening and restores the outer setting when it ends. Reopening a namespace elsewhere does not inherit an earlier opening’s local directive. Conflicting settings attached to one opening are errors; compatible repetition is redundant.
A module-level setting is one property of that generative module instance and
is inherited by its namespace openings unless they override it. Conflicting
module-level settings are errors. An explicit +++ final once declaration on a
type always overrides the inherited omission default.
This attachment is scope-bound rather than stateful source-order mutation, so moving an unrelated declaration or source file does not silently change later type policies. Complete Nothing behavior belongs to Zax Nothing instances.
Language-provided built-in types are not declared inside the opening and do not inherit this directive. Their Nothing policy is canonically trapping across modules, so one built-in pointer identity never changes vacancy meaning according to its lexical use site.
Files contribute to one ordered module
Every module has a module.zax source that establishes its source-file set and
source-file order. All selected files contribute declarations to the same
module root. A file is neither a namespace nor an identity layer.
Source order affects:
- whether a root has already been declared or forwarded;
- when an import and its injection become available; and
- global initialization sequencing, subject to future lifecycle rules.
It never breaks a name, callable, or operator ambiguity tie.
Declarations are module-internal by default, so moving one between files does
not inherently change visibility. Moving it earlier or later may make a
matching forward necessary or
redundant.
Wildcard source selection must eventually define a deterministic, host-independent order because global initialization may observe it. Exact source-list, wildcard, generated-source, and durable-build mechanics remain future build/dependency work.
Lookup from namespace source
An unqualified name searches the current namespace and then each containing namespace toward the module root:
namespace A {
a : Integer
namespace B {
b : Integer
D :: forward namespace
namespace C {
c : Integer
calculate final : (result : Integer)() = {
return a + b + c + D.d
}
}
namespace D {
d : Integer
namespace E {
e : Integer
}
}
}
}
Inside C:
cresolves inC;bresolves inB;aresolves inA;Dresolves to the forwarded namespace inB; andD.dremains pending untilDcompletes.
Bare e does not discover D.E.e. Namespace members are not flattened into
their parent.
Eligibility is applied while choosing a lexical scope. A scope containing only ineligible declarations does not block an eligible outer declaration. Once a scope has an eligible set, lookup stops there and applies its ordinary unique/family/ambiguity rules.
Explicit qualification fixes its root. A missing or inaccessible suffix never falls outward:
Module.A.B.D.E.e
The complete general algorithm, including aliases, shadow permission, pending suffixes, and diagnostics, is defined by declarations and bindings.
Named import, direct exposure, and collisions
The ordinary named form is:
Geometry :: import Module.MyGeometryModule
The direct form exposes eligible exported root declarations into the surrounding namespace:
:: import Module.MyFoundationModule
Direct exposure deliberately accepts a larger source-compatibility surface. Adding an export to the imported module may introduce another visible name or callable candidate.
Collision provenance matters
Two direct declarations in one module namespace are an immediate error except for a category that explicitly combines them, such as:
- a legal callable overload family;
- namespace reopening; or
- a forward and its matching completion.
Imported or exposed declarations retain different provenance. Several declarations may occupy one exposed spelling until source demands it:
:: import Module.FirstGeometryModule
:: import Module.SecondGeometryModule
point : Point
// error when both imports export distinct Point declarations
The rules are:
- exact repeated exposure of one declaration coalesces by identity;
- distinct imported declarations are ambiguous when used;
- legal callable members may form one approved overload family;
- namespaces from different module instances never merge by spelling;
- a local declaration does not win because it appears earlier; and
- qualification or selective exposure is the stable repair.
The compiler retains declaration provenance; source order does not infer it.
Generative imports and injection
An import creates a new module instance even when another import uses identical source and options. The instance includes:
- one resolved source identity;
- its complete injected declaration environment;
- declarations produced while that source is processed; and
- individualized module-global state.
Compilation may reuse work when semantics remain unchanged. Cache reuse does not merge module or declaration identity and does not share state.
Inject declarations before module source
An import body inserts declarations into the target root before the target’s own source resolves:
ThirdParty :: import Module.ThirdPartyModule {
UseComplexMath :: alias namespace Module.MyComplexMathImport
Options :: type {
debug final : readonly = true
}
}
Injected declarations behave as if the target module author wrote them:
- they use target-module ownership and visibility;
- their newly declared identities belong to that generative module instance;
- they participate in target direct-declaration collision rules; and
- a target declaration with the same direct name is a duplicate error.
Their special capability is lexical access to declarations at the injection
site. In the example, Module.MyComplexMathImport resolves in the importer.
UseComplexMath preserves that exact namespace and module-instance identity.
This supports deliberate dependency sharing:
MyType :: type {
foo : Integer
bar : String
}
ThirdPartyA :: import Module.ThirdPartyModule {
UseMyType :: alias type Module.MyType
}
ThirdPartyB :: import Module.ThirdPartyModule {
UseMyType :: alias type Module.MyType
}
If both aliases are exported, these denote one canonical type identity:
mine : MyType
fromA : ThirdPartyA.UseMyType = mine
fromB : ThirdPartyB.UseMyType = fromA
Declarations that ThirdPartyA and ThirdPartyB define themselves remain
distinct.
An injected import or exposure may contribute several imported candidates with one name. Those declarations retain imported provenance and use the demand-time ambiguity rule. Direct declarations written in the injection body do not.
A module can detect whether its importer injected something, and supply a fallback only when it did not:
if compile !wellformed{ Module.UseComplexMath } {
UseComplexMath outward :: import Module.DefaultComplexMath
}
wellformed{...}
asks the question under the imported module’s own root. if compile makes the
choice while compiling, and the
outward import becomes a
declaration of the module only when the fallback is selected. The answer is part
of that module instance’s semantic inputs: a different injection produces a
different instance.
Including partials in an imported type
An injection body is also how an importer adds stored members to a type in the
imported module, when that type writes seal open storage. It can include a
partial written elsewhere in the importer:
Library :: forward module
namespace MyNamespace {
MyTagging :: partial Library.MyCatalog {
tag : String = "none"
}
}
Library :: import Module.LibraryDefinition {
Module.MyNamespace.MyTagging :: own partial
}
own partial includes the partial’s storage and hook fulfillments in that
module instance’s MyCatalog without making the partial’s names visible to the
library’s source, so the library’s own code keeps selecting exactly what it did
before. expose partial in the same position would also make the partial
visible to all of the library’s source. It requires an
intent<injected-partial-exposure> acknowledgement because it can change
selections in code the importer cannot see. A partial defined directly inside
the injection body behaves like own partial.
Partials declared in a type’s own module are included automatically. Complete inclusion, ordering, and lifecycle rules belong to Zax partials.
Import cycles
Recursive import expansion must terminate. A repeated module dependency on one active import ancestry is an error, including a repeat reached through injection:
A -> B -> A
when both A entries count as the same dependency. A diamond remains legal:
A -> B
A -> C -> B
Generative instance identity cannot itself define repetition because every import creates another instance. Exact dependency work must decide whether the cycle key uses resolved source/content identity, repository identity, selected version, injection environment, or another stable combination. In particular, two versions of one repository are not yet classified as necessarily the same or necessarily different for cycle detection.
This owner establishes only that source cannot require unbounded recursive module instantiation. Exact repeat identity, dependency acquisition, version solving, locking, and cache mechanics remain future build/dependency work.
Visibility and export baseline
Declarations are module-internal by default. Every ordered source file in that module instance may use them after declaration or forward availability.
Nothing crosses an import unless it is explicitly exported. Importing a module does not automatically re-export that module or its declarations.
A module may explicitly export a named import binding. Downstream source then reaches that same generative instance through the exported path rather than creating another import automatically.
Exporting with directives
Export is written with the export
compiler directive. Attached, it exports one
declaration:
[<export>]
MyPoint :: type {
x : I32
y : I32
}
As a range, [<export=always>]
exports the declarations written after it, within one opening or file, until
[<export=never>] or the end of that region:
// file: api.zax
[<export=always>]
open final : ()() = { // exported
}
close final : ()() = { // exported
}
Math :: import Module.Math // not exported: ranges never export imports
[<export>]
Geometry :: import Module.Geometry // exported: an explicit attachment
A range exports only declarations written directly inside it. It never exports:
- import bindings or aliases, which always need an attached
[<export>], so importing never re-exports by itself; - declarations injected by an importer, or added by partials declared elsewhere; or
- the contents of nested namespaces, which need their own range or attachment.
A range ends at its end marker, at the closing brace of its opening, or at the end of its file. Reordering or moving files therefore never exports something by accident.
Exact private, selective export, alias export, and re-export option sets
remain future visibility work. That work inherits these constraints:
- module-internal is the default;
- export is explicit;
- import is not transitive export;
- visibility is eligibility, not match quality;
- inaccessible candidates do not compete as worse alternatives; and
- order never breaks an export collision.
Directive state stops at an import
No directive state crosses an import. An imported module begins with its own
defaults and its own source, whatever deprecation, export, or panic-check state
the importer is in. Source files brought in by inclusion are different: they
begin with the state in effect where they are included. See
directive ranges.
Namespace and module aliases
An exact namespace alias adds another path to one namespace identity:
LocalMath :: alias namespace Module.MyComplexMathImport
It does not copy declarations, create module state, grant reopening authority, or structurally merge same-named namespaces.
An import binding is itself the local name of a generative module root. An exported import binding preserves that module-instance identity.
Type, variable, reshape, and literal-operator aliases are declared by their domain forms and taught by declarations and bindings.
Forward module and namespace roots
A namespace root can be promised before its declaration:
Tools :: forward namespace
A qualified forward may be written from another namespace while declaring its name in an existing owned namespace:
namespace A {
namespace B {
}
}
namespace C {
A.B.PendingType :: forward type
usePending final : ()(
value : A.B.PendingType
) = {
}
}
namespace A.B {
PendingType :: type {
}
}
A uses ordinary root lookup; Module.A.B.PendingType is the explicit-root
form. Every containing component through A.B must already be a namespace or
matching namespace forward. The declaration belongs only to A.B; physical
placement in C creates no short C.PendingType.
A module import root has its own category:
ThirdParty :: forward module
ThirdParty :: import Module.ThirdPartyModule
forward module does not instantiate the dependency. It establishes a
source-ordered module-root anchor so qualified paths can remain pending. The
later import, including its injection body, creates the generative instance and
completes those paths.
A matching forward is legal even when no use occurs before completion. Tooling may lint it as redundant. General forward categories, completion, aliases, and errors are defined by declarations and bindings.
Costs
Programmers must be able to discover:
- source acquisition and verification;
- processing and static execution performed for an import;
- each generative module instance;
- declarations and global state duplicated by separate imports;
- exact dependencies and state deliberately shared through injection;
- direct-exposure collision analysis;
- pending work caused by forwards and incomplete namespace paths;
- global initialization and destruction governed by future lifecycle rules; and
- rebuild/cache invalidation caused by export, injection, source-list, or dependency changes.
No representation, cache, lowering, or linker strategy is promised here.
Diagnostics
Diagnostics should distinguish:
- unknown root from a known forwarded root;
- a namespace path whose intermediate component is not a namespace;
- namespace declaration in a function, block, or type body;
- unauthorized reopening of an imported or protected namespace;
- missing self-name
shadowableon the opening that introduces a shadow; - same-scope direct duplicate from demand-time imported ambiguity;
- inaccessible declaration from absent declaration;
- missing suffix below a complete root from a pending suffix;
- mismatched or uncompleted namespace/module forward;
- distinct generative declarations with equal spelling or shape;
- target-source collision with an injected direct declaration;
- recursive or unbounded module import, including an injection-created edge;
- non-exported declaration at an import boundary; and
- missing or conflicting explicit re-export.
Exact identifiers and presentation remain later diagnostics design.
Source stability
Compatibility events include:
- moving a declaration before or after a use that depends on a forward;
- changing source-file or wildcard expansion order;
- moving a declaration between module or namespace owners;
- adding, removing, renaming, or changing visibility of an export;
- replacing a named import with direct exposure;
- changing an import’s source identity or injection body;
- replacing separate module instances with one exact injected dependency, or the reverse;
- changing a namespace alias target;
- adding a declaration that collides with injected source;
- making a private declaration eligible; and
- adding another imported candidate under an exposed name.
Source, file, declaration, import, module, generation, and cache order never silently resolve an ambiguity.
Boundaries and maturity
This document is current conceptual design, not a formal grammar, build contract, package-manager specification, ABI promise, or compiler mapping.
Future work owns:
- exact
private, selective export, alias export, and re-export options; - package fetching, Git resolution, versions, locking, canonical content identity, and cache control;
- module/global initialization, destruction,
once, and concurrency; - open namespace authority;
- source and declaration reflection; and
- dynamic loading and runtime name lookup.
Those mechanisms must preserve protected local roots, source-ordered name availability, generative module identities, exact injected sharing, explicit export, eligibility-before-preference, and order-independent ambiguity.