Skip to content

Prelude

Accepted

Accepted for the V1 prelude placement rule and compiler-provided surface; implementation verification is tracked in Scope Backlog.

Catalyst separates the automatically available prelude namespace from the broader standard library (std). The prelude is the small semantic surface that makes ordinary language features type-check. std is the larger explicitly imported library for algorithms, containers, IO, platform services, allocator implementations, and higher-level utilities.

The prelude is compiler-owned, but its public declarations are ordinary Catalyst declarations where possible. Some prelude APIs are backed by compiler primitives or reflection hooks, and the compiler may store or cache the prelude in an implementation-specific form. The public surface should remain normal source-level names.

The prelude namespace is compiler-provided and outside the ordinary package dependency graph. It is available before normal package resolution so that core names such as module, core contracts, attributes, Box, and allocation/error primitives can be used to resolve and type-check ordinary code.

prelude is versioned with the compiler and is not a module. std is bundled with the compiler toolchain as a normal module with its own module.toml, version, and main .ct file.

Every ordinary .ct file receives a compiler-provided initial scope containing the public prelude declarations and public prelude impls. This initial scope behaves like an import for conflicts and lookup, but it is not literally import module("prelude") because prelude is not a module.

The namespace name prelude is also automatically bound to the compiler-owned prelude namespace, so fully qualified prelude names are always available unless a future compiler mode disables automatic prelude behavior.

Prelude declarations follow the same lazy top-level analysis rule as ordinary declarations. Making a public prelude name visible does not fully evaluate its initializer unless user code or a validation root demands it. The compiler toolchain should still validate the intended prelude surface in its own build and test pipeline by demanding the relevant public declarations.

Prelude declarations also have stable fully qualified names:

prelude.Box(T)
prelude.Iterator(Item)
prelude.PartialEq(T)
prelude.PartialOrd(T)
prelude.Add(Rhs, Out)
prelude.Sub(Rhs, Out)
prelude.Mul(Rhs, Out)
prelude.Div(Rhs, Out)
prelude.Rem(Rhs, Out)
prelude.Neg(Out)
prelude.Allocator
prelude.module("std")

Placement Rule

A name belongs in the prelude only when at least one of these is true:

  • source syntax lowers to it
  • core type checking needs the name to state a semantic rule
  • the name is a tiny universal type or contract used across ordinary programs
  • the name is needed to make resource ownership, allocation, or dynamic dispatch expressible without special cases

A name belongs in std when it is useful but not required to understand core language semantics:

  • algorithms such as sort, min, max, and clamp
  • concrete collections beyond arrays, slices, and ranges
  • concrete allocator implementations
  • IO, filesystem, clocks, threads, processes, networking, and platform APIs
  • hashing algorithms and hasher implementations
  • convenience wrappers around lower-level prelude concepts

The prelude should stay small. Adding a prelude name raises the stability bar because every module sees it by default.

Prelude Surface

The V1 prelude includes primitive numeric type factories and aliases:

SignedInteger(bits)
UnsignedInteger(bits)
Float(bits)
i1..i128
u1..u128
isize
usize
f32
f64

SignedInteger(bits), UnsignedInteger(bits), and Float(bits) are prelude declarations whose bodies call public compiler primitive-type-construction intrinsics. Their public names live in the prelude, while the compiler validates supported widths and owns the canonical primitive Type identity, layout, ABI, literal typing, arithmetic lowering, and target behavior. Their bits parameters have type comptime_int so the prelude can define primitive aliases before aliases such as u8 exist. The familiar names such as i32, u64, and f32 are prelude aliases for those canonical factory applications, not wrapper types.

The V1 prelude includes the core contracts that syntax and generic APIs depend on:

Indexable(T)
MutableIndexable(T)
Sequence(T)
MutableSequence(T)
Iterator(Item)
Iterable(Item)
MutableIterable(Item)
PartialEq(Other)
Eq(T)
PartialOrd(Other)
Ord(Other)
Add(Rhs, Out)
Sub(Rhs, Out)
Mul(Rhs, Out)
Div(Rhs, Out)
Rem(Rhs, Out)
Neg(Out)
Disposable

Primitive numeric equality and ordering conformances are ordinary prelude impls over SignedInteger(bits), UnsignedInteger(bits), and Float(bits). Their bodies call compiler comparison intrinsics so static operator lowering can compile to primitive comparisons without a function-call or dispatch penalty.

Primitive numeric arithmetic conformances are also ordinary prelude impls over SignedInteger(bits), UnsignedInteger(bits), and Float(bits). Their bodies call compiler arithmetic intrinsics so contract-backed arithmetic operators compile to primitive arithmetic after specialization and inlining.

The V1 prelude includes helper iterator types used by the default sequence implementations:

IndexIterator(Seq, Item)
MutableIndexIterator(Seq, Item)

These helpers are not compiler magic, and sequence implementations may override the defaults with their own static iterator types.

The prelude includes the comptime-only namespace type and module-loading functions:

Namespace
module(comptime name: []const u8) Namespace
include(comptime path: []const u8) Namespace

Ordering is a prelude enum because PartialOrd and Ord use it directly:

const Ordering = enum {
  less,
  equal,
  greater,
}

The prelude includes ownership and dynamic-dispatch primitives that are ordinary library types but required by the accepted semantics:

Box(T)
box(T, alloc, value)
box_dyn(C, I, alloc, value)
Box(dyn C).create_from_dyn(I, alloc, value)
Allocator
Error
AllocError

Allocator is a prelude semantic contract, not a concrete erased handle. V1 allocator-taking APIs use static contract dispatch by default:

alloc: *impl Allocator

Concrete allocator policies such as arena, fixed-buffer, page, testing, and general-purpose allocators belong in std.mem and implement Allocator. Dynamic allocator erasure through *dyn Allocator is valid only when an API explicitly wants a runtime-erased allocator boundary; it is not the default shape for Box, box, or box_dyn.

iter_dyn and iter_dyn_mut are the important exception: because they are dynamic contract operations, their allocator parameter cannot use the static *impl Allocator parameter form and still remain dyn-safe. They take explicit erased allocator authority, alloc: *dyn Allocator, and create boxed dynamic iterators through Box(dyn C).create_from_dyn(I, alloc, value). Box(T).create, box, and box_dyn keep static allocator dispatch.

The V1 Allocator contract exposes a small low-level storage core plus static-only typed helpers:

const Allocator = contract {
  fn allocate(self: *Self, size: usize, align: usize) *opaque!AllocError
  fn deallocate(self: *Self, ptr: *opaque, size: usize, align: usize) void

  fn create(self: *Self, comptime T: Type) *T!AllocError
    if ContractSurface.current().is_static()

  fn destroy(self: *Self, comptime T: Type, ptr: *T) void
    if ContractSurface.current().is_static()

  fn alloc(self: *Self, comptime T: Type, count: usize) []T!AllocError
    if ContractSurface.current().is_static()

  fn free(self: *Self, comptime T: Type, items: []T) void
    if ContractSurface.current().is_static()
}

Raw allocator operations carry layout explicitly:

  • Allocator.allocate(size, align) returns raw *opaque storage with at least the requested size and alignment.
  • Allocator.deallocate(ptr, size, align) must receive the same layout contract used for the allocation.
  • Passing the wrong size or alignment is illegal behavior; allocators may trap in checked or debugging configurations when they can detect a mismatch.
  • The raw dynamic surface carries layout explicitly so allocator implementations do not need hidden per-allocation metadata.

Typed allocator helpers derive layout from T:

  • Allocator.create(T) allocates uninitialized storage for one T by deriving size and align from T.layout(), calling allocate, and recovering *T with raw.cast(T).
  • Allocator.destroy(T, ptr) frees storage for one T by erasing ptr to *opaque and passing T.layout().size and T.layout().align to deallocate.
  • Allocator.destroy(T, ptr) does not run Disposable.dispose.
  • Object lifecycle belongs to owner APIs such as Box(T).create and Box(T).dispose, not to the allocator contract.

Manual initialization follows ordinary pointer assignment:

  • writing through ptr.* = value starts the T lifetime in uninitialized storage;
  • reading before initialization is illegal behavior and should be diagnosed when statically obvious;
  • reassigning through ptr.* after initialization is ordinary assignment and must respect copy, move, and resource rules.

V1 spells typed helper type arguments explicitly, such as alloc.destroy(Buffer, ptr) and alloc.free(u8, items). Inferring T from ptr: *T, items: []T, or value: T is deferred to CEP-0016: Generic Parameter Inference.

Core error mechanics and the top error-set type are language/prelude surface. Error is the prelude binding for the compiler-owned top error-set type value:

const Error: Type = Compiler.top_error_set()

Compiler.top_error_set() is an open compiler intrinsic that returns the canonical top error-set type value. Sema operates on that value, not on the spelling Error.

AllocError is ordinary prelude source because allocation failure appears in accepted core APIs such as Box(T).create, box, box_dyn, iter_dyn, and iter_dyn_mut:

const AllocError = error {
  OutOfMemory,
}

Domain error sets such as IO, filesystem, parsing, networking, and platform errors belong in std or user modules.

Optional semantics are owned by the language reference, not by a prelude Option type. A small set of universal optional helpers may live in the prelude if they prove necessary, but rich optional combinators such as map, and_then, and unwrap_or belong in std or remain deferred. See Optional Types.

Result(T, E) is a canonical std type, not prelude. Catalyst error returns provide the core propagation model, so Result is not needed for ordinary fallible calls. The standard library should still provide one official result container for APIs that need to store, aggregate, or pass outcomes as values, avoiding many incompatible user-defined equivalents.

Box(T) is a prelude resource type, not a compiler-owned type. It is implemented using the general allocation, ownership, dyn, and reflection primitives. The compiler knows the language rules for dyn, vtable metadata, resource copyability, and move semantics; it does not special-case Box operations.

opaque and dyn C are language type syntax, not prelude declarations. Box(dyn C) is the V1 prelude-owned dynamic value type because accepted dynamic iteration and owned erasure need one canonical owner. Other erased-owner helpers or type-erasure containers belong in std or future libraries unless promoted by a concrete core-language need.

The prelude includes first-class range value types because range syntax produces them outside slicing contexts:

Range(T)
RangeInclusive(T)

Range syntax is compiler syntax, but these produced values are ordinary copyable prelude types with Iterable(T) implementations for integer endpoint types in V1.

The prelude includes compiler-facing attributes and declarations that define core declaration metadata:

@attribute
@resource
@callconv
@export
@extern
@link_name
@repr
@align
@deprecated

These attributes are still resolved as prelude names. The compiler recognizes the bootstrap registration protocol for attributes, but ordinary attribute behavior should be inspectable through the declared prelude/provider surface. The canonical V1 attribute set and provider rules are documented in Prelude Attributes.

Standard Library Boundary

std placement is documented in Standard Library. Compiler assumptions about the prelude are documented in Compiler Boundary.