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

Declarations

There are four declaration kinds in a .prim file: const, enum, type (alias), and namespace (one-liner override). This page covers each.

Constants

<type> <NAME> = <literal>

The type comes first; the name is SCREAMING_SNAKE_CASE; the value is a literal of the declared type. One per line; no semicolons.

duration TIMEOUT     = 30s
u32      MAX_RETRIES = 5
u64      MAX_UPLOAD  = 100MiB
string   API_VERSION = "v3"
bool     STRICT_MODE = true

The type is mandatory — there’s no inference at any level. This is a deliberate choice; see Overview for the rationale.

Doc comments attach to the next declaration:

/// How long the gateway waits before bailing on a slow upstream.
///
/// Bumping this value also requires bumping the load-balancer's
/// idle-timeout — they must stay aligned.
duration UPSTREAM_TIMEOUT = 30s

/// lines accumulate until the declaration; one blank line detaches the doc block (it becomes a standalone // comment in the formatter output).

Alignment within groups

Consecutive declarations with no blank line between them form a group. The formatter aligns the type, name, and = columns across the group:

duration TIMEOUT     = 30s
u32      MAX_RETRIES = 5
u64      MAX_UPLOAD  = 100MiB

A blank line breaks the group; doc comments don’t.

Enums

enum Name { Variant, … }

By default, variants are string-tagged — they serialize as their PascalCase name in generators that distinguish.

/// Operation status.
enum Status {
    Pending,
    Active,
    Done,
}

For an integer-backed enum, add : <int-type> after the name:

enum LogLevel: u8 {
    Debug = 0,
    Info  = 1,
    Warn  = 2,
    Error = 3,
}
  • Backing type must be an integer primitive (i8/i16/i32/i64/u8/u16/u32/u64).
  • Variants without an explicit value get auto-assigned 0, 1, 2, ….
  • Enum bodies follow the same group-alignment rules as constants — the formatter aligns the = column across variants.

Trailing commas are accepted on the last variant; the formatter always emits one for multi-line enum bodies.

Type aliases

type Port           = u32
type ServiceConfig  = map<string, Port>
type Color          = tuple<u8, u8, u8>

Aliases are real first-class types. They’re emitted as standalone type declarations in the generated code so the alias’s name shows up in hover docs, IDE tooltips, and so on.

To suppress emission and inline the underlying type at use sites, mark with @inline:

@inline
type Bytes32 = u64

Aliases participate in cross-file resolution exactly like enums: sibling files in the same namespace see them by bare name; other namespaces use a fully qualified path or a use import.

Alias chains (type A = B, type B = C) are resolved transitively at IR time, so generated code never contains a chain.

namespace

Each file belongs to one namespace. The default comes from the file’s path; the override looks like:

// constants/legacy/old_metrics.prim
namespace metrics::v1

Rules:

  • One per file. Zero allowed (then the path-derived default is used).
  • If present, must be the first non-comment item.
  • Single line, no braces, :: as separator.

You usually shouldn’t need this. The path-derived default is the recommended way to organize. Reach for an explicit namespace only when:

  • A file’s path doesn’t reflect where its declarations should live (the legacy-rename case above).
  • Two files at different paths need to share a namespace and you don’t want to move them.

Order in the file

If a namespace override is present, it must come first. After that, order is free — declarations don’t have to come before they’re used, because resolution happens after the whole file (and project) is parsed. The formatter doesn’t reorder declarations.

use imports are an exception: they go at the top of the file, after the optional namespace line, before any other declaration. The formatter sorts and merges them; see use statements.