Skip to content

Comptime

Accepted

Accepted for the V1 deterministic compile-time evaluation model; advanced comptime, host access, generated declarations, and broad source-text access are deferred.

V1 comptime is deterministic compile-time evaluation for explicit and demanded comptime contexts. It produces inspectable semantic values and diagnostics, not hidden source rewrites.

comptime is Catalyst's primary abstraction mechanism. It should replace macros, template languages, reflection systems, code generators, and type-programming hacks.

Comptime Requirements

Comptime execution should be:

  • deterministic
  • sandboxed
  • cacheable
  • explicit in source
  • visible to tooling

The execution substrate for V1 comptime is the shared IR interpreter. That keeps compile-time execution aligned with normal language semantics instead of creating a separate evaluator. Compiler-owned semantic objects, public compiler intrinsics, and prelude bootstrap are documented in Comptime Bootstrapping and Compiler Objects.

Comptime evaluation is demand-driven unless source explicitly forces it with a comptime expression or block. Top-level declaration initializers follow the lazy top-level analysis rule: they are not evaluated merely because their namespace is visible, but only when a demanded semantic check needs their value.

When sema or lowering needs a comptime result, the compiler evaluates the required declaration or expression under the active compiler context and may reuse that result during the same compilation only when the evaluated semantic instance, comptime arguments, target, safety mode, captured scope context, and active in-memory semantic environment are the same. V1 does not define incremental comptime-cache validity across compiler runs; implementations may discard comptime caches between runs or after source/module changes. Incremental cache keys and dependency invalidation are deferred to CEP-0061: Incremental Comptime Cache Invalidation.

Comptime dependency cycles are compile errors with deterministic diagnostics.

All comptime parameters must use comptime explicitly:

fn first(comptime T: Type, items: []T) T

Evaluation Contexts

V1 comptime runs in semantic instances, not in runtime calls. A comptime result belongs to a module, declaration, function, contract, type, attribute, or forced expression/block instance with explicit comptime inputs and recorded dependencies.

Runtime call sites never add hidden comptime inputs. A forced comptime block inside a runtime-callable function is evaluated for the enclosing semantic function instance. If different callers need different compile-time behavior, that variation must be expressed through explicit comptime parameters, lexical comptime constants, compiler context values, or other recorded compile-time dependencies.

V1 comptime contexts include:

  • type expressions and type factories;
  • contract factories and contract operation guards;
  • structural constraints such as satisfies(...) and mutable_fields(...);
  • predicate-producing reflection such as T.implements(C) and T.satisfies(Shape);
  • declaration guards and comptime-known if conditions;
  • attribute provider registration and evaluation;
  • declaration-scope module(...) and include(...);
  • Scope.caller() and Scope.current() where reflection APIs require scope-sensitive lookup;
  • forced comptime expressions and blocks;
  • calls to comptime fn declarations;
  • compiler diagnostics through Compiler.note, Compiler.warn, and Compiler.err.

Ordinary functions may be called during comptime evaluation when the call path satisfies the same deterministic comptime rules. An ordinary function does not need to be declared comptime fn merely to be interpreted at compile time.

Public API validation demands the declarations needed by public signatures, public impls, and public namespace re-exports. Toolchain or package validation may add broader demand roots to force analysis of additional public declarations, but ordinary user compilation does not fully evaluate every visible declaration by default.

Forced Comptime

V1 accepts forced comptime expression and block forms:

const n = comptime compute_len()

comptime {
  Compiler.note(.{ .message = "checking Buffer layout" })
}

comptime expr evaluates expr during compilation. comptime { ... } is a block expression evaluated during compilation. A statement-position block may evaluate to void; a value-position block produces the block's final value using ordinary block expression rules.

Forced comptime does not inject declarations into the enclosing namespace, rewrite source, or change surrounding API shape except through existing explicit mechanisms such as declaration guards, produced Type values, or attribute target setters. Bindings declared inside a forced block are local to that block.

The result of a forced comptime expression may be a runtime-storable value after evaluation:

const size = comptime fibonacci(10)
var data: [size]u8 = undefined

Runtime values cannot flow into forced comptime:

fn bad(n: u32) u32 {
  return comptime fibonacci(n)
}

Comptime Functions

comptime fn declares a function that is callable only during comptime evaluation:

comptime fn fibonacci(n: u32) u32 {
  if n < 2 {
    return n
  }

  return fibonacci(n - 1) + fibonacci(n - 2)
}

Calling a comptime fn implicitly forces comptime evaluation of that call. All arguments and loaded values used by the call path must be available in the current comptime environment:

fn ok() u32 {
  return fibonacci(10)
}

Runtime-known arguments are rejected:

fn bad(n: u32) u32 {
  return fibonacci(n)
}

comptime fn is a callability restriction, not a new evaluator. Its body is checked and lowered like an ordinary function body, and it receives no extra capabilities. It cannot access host resources, generate declarations, mutate persistent global comptime state, or bypass deterministic failure rules.

A comptime fn may be referenced as a comptime-known function item and passed through comptime parameters. It does not coerce to a runtime function pointer and is not a runtime-callable callback. V1 allows comptime fn for free functions and inherent helper functions. Comptime-only contract operations are deferred.

Ordinary fn declarations remain callable at comptime when a forced or demanded comptime context evaluates them with comptime-known inputs.

Constants and Runtime Storage

const means immutable binding. It does not by itself mean compile-time value.

A const binding is a comptime binding when its initializer or type is comptime-only, or when the initializer is explicitly forced or demanded by a comptime context:

const T = i32
const std = module("std")
const can_compare = T.implements(PartialEq(T))
const n = comptime fibonacci(10)

Type, Namespace, Type.Predicate, Scope, function item values, comptime_int, and comptime_float are comptime-only in V1. They are valid in comptime constants, const bindings used as comptime values, and explicit comptime parameters, but not in runtime storage.

A const binding with a runtime-storable type is immutable runtime storage unless a comptime context demands the value:

const x: i32 = 123
const y = runtime_call()

The compiler may constant-fold runtime-storable expressions as an optimization, but optimization folding does not make a value usable in type expressions, declaration guards, module/include paths, attributes, or other language-level comptime positions. Authors must use a demanded comptime context or explicit comptime when the value is part of source semantics.

If a binding's inferred type is neither runtime-storable nor valid as a comptime value, sema rejects it with a deterministic diagnostic.

Tooling Contract

Comptime-produced semantic information must remain visible to tooling. IDEs should be able to inspect inferred types, conditional declaration availability, attribute-produced metadata, available methods, and constraint satisfaction.

If tooling cannot understand a comptime pattern, that is a language design problem, not only an IDE problem.

Comptime validation uses the compiler diagnostic APIs Compiler.note, Compiler.warn, and Compiler.err. See Diagnostics.

Interpreter Contract

Comptime interpretation must be deterministic, sandboxed, resource-limited, and cacheable. It must not depend on host-global mutable state or hidden IO. Host interaction should require explicit host-access values or APIs.

Comptime execution uses target semantics, not host semantics. usize, isize, layout reflection, alignment, ABI checks, pointer-sized integer behavior, and checked safety traps are evaluated for the active compilation target and safety mode. See Target and Safety Modes.

Comptime code may use ordinary language constructs once they lower to verified IR, including locals, aggregate values, address-taking, pointer parameters, loads, stores, and slices. Comptime mode restricts host effects and nondeterminism; it is not a separate pointer-free or aggregate-free subset of Catalyst.

Comptime evaluation may mutate evaluation-local memory. It may not mutate runtime storage or hidden persistent compiler/global state. Attribute providers are the explicit V1 exception: they may mutate the current target's semantic metadata through phase-owned setter APIs before that target is frozen.

Unsupported host access, external function execution, runtime value dependencies, resource exhaustion, Compiler.err, and checked safety traps during comptime evaluation are deterministic compile errors. Diagnostics should identify the demanded or forced comptime site, the failing call path when available, and a stable failure kind.

Compiler.note and Compiler.warn are execution-time effects of comptime evaluation. Successful cached results are not required to replay notes or warnings. Stable user-facing diagnostics that must appear independently of evaluation caching should be produced by sema, lints, or phase diagnostics instead of relying on cached comptime replay.

Comptime resource limits are compiler robustness safeguards, not source-level tuning knobs. V1 has no source syntax for raising step, branch, recursion, or memory limits. The compiler should choose defaults high enough for ordinary V1 comptime use; tests and build tools may configure limits as implementation policy.

Caching is semantically invisible except for execution-time note/warn effects. Within one compilation, the compiler may reuse a comptime result only when the semantic instance, explicit comptime arguments, target, safety mode, captured scope context, and active semantic environment match. If the compiler cannot prove a cached result is valid, it must re-evaluate. Cross-run incremental cache validity, file/module content identities, and fine-grained dependency invalidation are deferred to CEP-0061: Incremental Comptime Cache Invalidation.

V1 Boundaries

Deferred from V1:

  • general host access, including environment variables, current time, randomness, network, shell commands, and arbitrary file reads;
  • source-level quota controls;
  • broad source-text access and public expression/value reflection;
  • generated declarations, generated impls, AST rewriting, source rewriting, and declaration injection from comptime blocks;
  • persistent global comptime state;
  • stack-frame inspection beyond direct Scope.caller();
  • comptime-only contract operations;
  • advanced predicate logic such as negative facts, runtime value narrowing, and richer fact algebra.

Generated declarations and impls are tracked by CEP-0009: Generated Declarations and Impls. Advanced comptime items outside V1 are tracked by CEP-0025: Compiler Intrinsic Expression Syntax, CEP-0026: Comptime Host Access, CEP-0027: Source Text and Value Reflection, CEP-0028: Comptime Resource Limits, and CEP-0029: Predicate Fact Algebra.

Non-Goals

Catalyst should avoid:

  • a separate macro language
  • runtime reflection as the main abstraction tool
  • a separate type-level programming language
  • hidden code generation that cannot be inspected