Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .ts file per namespace (plus an index.ts re-exporting each one). Cross-namespace references become real ES import statements at the top of each file.
  • Rust — primate emits a single .rs file with one pub mod <ns> { ... } per namespace. Cross-namespace references become super::<other>::X.
  • Python — primate emits a package directory with one .py per namespace and an __init__.py. Cross-namespace references become from .<other> import X.

So if limits.prim references LogLevel from logging, you get the right import or path at the consumer site for free.