Cross-namespace types
Real projects have more than one file. This page shows how to organize types across namespaces and reference them from elsewhere.
File layout drives namespaces
The recommended pattern: let the directory layout determine the
namespace. Don’t write namespace foo at the top of files.
constants/ ← input
├── net/
│ ├── limits.prim → namespace `net::limits`
│ └── headers.prim → namespace `net::headers`
├── log/
│ └── levels.prim → namespace `log::levels`
└── jobs.prim → namespace `jobs`
Files with the same parent share a namespace. Sibling files in
net/ see each other’s enums and aliases by bare name.
Cross-namespace by qualified path
Reference a type from another namespace by its fully qualified path:
// constants/jobs.prim
log::levels::LogLevel DEFAULT_LEVEL = Info
Generated TypeScript imports the type from the right namespace automatically.
Or with use for ergonomics
If you reference a name often, bring it into scope with use:
// constants/jobs.prim
use log::levels::LogLevel
use net::limits::{Port, IP}
LogLevel DEFAULT_LEVEL = Info
Port COORDINATOR = 9000
IP DEFAULT_BIND = "0.0.0.0"
use is purely an ergonomic — it has no effect on generated code.
See use statements for the rules.
A shared types file
Group types that multiple namespaces need into a core/types.prim
or similar:
// constants/core/types.prim
/// Used everywhere a network port is named.
type Port = u32
/// IPv4 or IPv6 address.
type IP = string
/// Severity, integer-backed for fast filtering.
enum LogLevel: u8 {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
Other files import via qualified path or use:
// constants/services/api.prim
use core::types::{Port, LogLevel}
Port API_PORT = 8080
LogLevel API_LOG_LEVEL = Info
When to override the namespace
The escape hatch (namespace foo::bar at the top of a file) is for
the rare case where path-derived doesn’t fit:
// constants/legacy/old_metrics.prim
namespace metrics::v1
// — overrides the path-derived `legacy::old_metrics` so we can keep
// serving these on the existing `metrics::v1` API.
Use it sparingly. If you find yourself overriding more than once or twice, that’s a signal the directory layout doesn’t reflect your intended organization — move the files instead.
Same name in two namespaces
primate allows the same type or constant name to exist in different namespaces. Within a single namespace, duplicates are an error.
// constants/net/limits.prim
type Port = u32
// constants/audio/limits.prim
type Port = u8 // OK — different namespace
If you use both at once into a third file, that’s an
import-collision error:
use net::limits::Port
use audio::limits::Port // ✗ `Port` is already imported from `net::limits`
Either use only one, or qualify both at the call site.
Where this matters in generated code
Each language preserves your namespace structure idiomatically:
- TypeScript — primate emits one
.tsfile per namespace (plus anindex.tsre-exporting each one). Cross-namespace references become real ESimportstatements at the top of each file. - Rust — primate emits a single
.rsfile with onepub mod <ns> { ... }per namespace. Cross-namespace references becomesuper::<other>::X. - Python — primate emits a package directory with one
.pyper namespace and an__init__.py. Cross-namespace references becomefrom .<other> import X.
So if limits.prim references LogLevel from logging, you get the
right import or path at the consumer site for free.