Skip to content

Comptime Bootstrapping and Compiler Objects

Accepted

Accepted for the V1 compiler-facing seam between source-level comptime, compiler-owned semantic objects, public compiler intrinsics, and prelude bootstrapping.

This page defines how V1 comptime code manipulates compiler-owned semantic values without creating a second evaluator or hiding compiler facilities behind privileged prelude-only APIs.

One Evaluator

V1 has one comptime evaluator: the shared IR interpreter.

When sema or lowering demands a comptime value, the demanded unit is checked, lowered to IR, verified, and executed by the shared interpreter in comptime mode. The interpreter must not inspect AST or SIR directly. If a comptime operation needs information that is missing from IR, the fix belongs in SIR-to-IR lowering, the IR schema, or an explicit compiler intrinsic handler.

There is no separate prelude evaluator, AST evaluator, macro evaluator, or special bootstrap expression evaluator. Lazy top-level analysis controls demand order; it does not define a second execution path.

Compiler-Owned Comptime Handles

Some V1 comptime values are compiler-owned semantic objects, not runtime structs and not user-forgeable aggregate values. In IR and interpreter state they are opaque typed handles backed by compiler-owned identities.

Compiler-owned comptime handles include:

  • Type;
  • Namespace;
  • Scope;
  • Type.Predicate;
  • reflection metadata values such as conformance, satisfaction, dyn-safety, function, field, layout, and attribute metadata;
  • Type.DynMetadata and its pointer/dispose descriptor metadata;
  • attribute target handles while an attribute provider is executing;
  • compiler context values such as Target and SafetyMode;
  • function item values.

These handles may be stored in comptime constants, passed through comptime parameters, returned from comptime-evaluated functions, and inspected through accepted reflection APIs. They are invalid in runtime storage unless a specific API produces a runtime-storable descriptor or value.

Compiler Intrinsics

Compiler intrinsics are ordinary public source-facing operations exposed through the Compiler namespace or through compiler-defined methods on compiler-owned comptime handle types. They are not private prelude APIs. Prelude code and user code use the same public tools.

Intrinsic declarations are seeded into the compiler-provided Compiler namespace, or attached as compiler-owned methods on compiler-owned handle types, with normal public signatures plus hidden intrinsic metadata. Sema resolves and type-checks intrinsic calls like ordinary calls; it does not dispatch behavior based on source spelling.

An intrinsic declaration records:

  • a stable intrinsic ID;
  • an implementation category;
  • effect and dependency classes;
  • context requirements;
  • result storage category when the result may be comptime-only;
  • compiler-owned provenance for reflection and diagnostics.

V1 has two intrinsic implementation categories:

  • comptime_host: the call is valid only in demanded comptime execution and lowers to a comptime-only host-call target consumed by the shared interpreter.
  • lowering_expansion: the call is public source syntax, but lowering expands it into ordinary IR operations or backend-visible facts before backend verifier acceptance.

An intrinsic may still be context-gated. A context-gated intrinsic is rejected when the required comptime, declaration-scope, source-loading, attribute-registration, or active attribute-target context is absent. Diagnostics should name the missing context rather than saying the API is private or prelude-only.

Named V1 compiler intrinsics that are already accepted include:

Compiler.signed_integer_type(bits)
Compiler.unsigned_integer_type(bits)
Compiler.float_type(bits)
Compiler.top_error_set()
Compiler.target()
Compiler.safety_mode()
Compiler.note(diagnostic)
Compiler.warn(diagnostic)
Compiler.err(diagnostic)
Compiler.module(name)
Compiler.include(path)
Compiler.register_attribute_provider(provider, spec)

The primitive type construction intrinsics validate their comptime_int arguments and return canonical primitive Type handles. The prelude factories are ordinary wrappers:

fn SignedInteger(comptime bits: comptime_int) Type {
  return Compiler.signed_integer_type(bits)
}

fn UnsignedInteger(comptime bits: comptime_int) Type {
  return Compiler.unsigned_integer_type(bits)
}

fn Float(comptime bits: comptime_int) Type {
  return Compiler.float_type(bits)
}

Other accepted compiler-object operations may be implemented as intrinsic handlers behind ordinary method syntax without freezing every metadata helper name on this page:

  • Type classifier and metadata operations such as T.is_*(), T.fields(), and T.layout();
  • visibility-aware reflection such as T.implements(C, scope), T.conformance(C, scope), T.satisfies(Shape, scope), and T.satisfaction(Shape, scope);
  • dyn-safety and dynamic metadata operations such as C.is_dyn_safe(), C.dyn_safety(), and I.dyn_metadata(C, scope);
  • namespace member lookup, namespace destructuring, import selection, module(...), and include(...);
  • Scope.current() and Scope.caller();
  • attribute target setters and metadata operations while the relevant target is still mutable.

If an operation needs compiler tables, visibility, semantic identity, target facts, diagnostics, source loading, or phase-owned mutation, it is an intrinsic handler. If it only computes over ordinary values or previously returned comptime handles, it is ordinary interpreted IR.

Intrinsic Lowering and Dispatch

Source syntax and SIR do not have a special intrinsic expression form. The parser sees ordinary call syntax, and Sema resolves the callee to a typed compiler-owned declaration or method. SIR records the resolved call target and its intrinsic metadata.

When a demanded comptime unit contains a comptime_host call, lowering emits a verified IR call whose target kind is comptime-only host intrinsic. The interpreter dispatches that target to the intrinsic handler in comptime mode. The backend verifier rejects host-intrinsic call targets.

When lowering sees a lowering_expansion call, it invokes the intrinsic's lowering handler. The handler receives typed operands and a narrow lowering context, then emits ordinary IR operations, constants, traps, checks, descriptor constants, or backend-visible facts. A lowering_expansion intrinsic may be used in runtime code or demanded comptime code because its result after lowering is ordinary IR.

Generic compiler intrinsic calls must not appear in verified backend IR. A comptime_host intrinsic may appear only in verified IR for a demanded comptime unit. A lowering_expansion intrinsic must be expanded before verifier acceptance for backend emission. If either category leaks past its allowed phase, that is an implementation error unless Sema has already identified a source-level misuse.

Primitive operation intrinsics used by prelude conformances, such as integer arithmetic and comparison helpers, are lowering_expansion intrinsics. For example, a source call shaped like Compiler.int_add(u32, u32, a, b) is type-checked as an ordinary call and then expands to checked integer-add IR for the selected result type and safety mode. It does not survive as a call in backend IR.

Intrinsic Handler Authority

Intrinsic handlers receive narrow context objects rather than raw compiler internals.

A comptime_host handler may receive:

  • typed interpreter argument values;
  • the current session facts needed by its declared dependencies;
  • current comptime demand identity and call stack for diagnostics;
  • access to explicitly allowed compiler-owned handle tables;
  • a diagnostic emitter;
  • a dependency recorder for cache soundness;
  • authority objects only when the active context grants them, such as source-loading authority, attribute-registration authority, mutable attribute-target authority, visible-scope authority, or contract-surface authority.

A lowering_expansion handler may receive:

  • its stable intrinsic ID;
  • typed SIR operands and lowered operand values or operand-lowering callbacks;
  • current target, safety, type, layout, and source-span facts allowed by its dependencies;
  • a result type or expected type already selected by Sema;
  • an IR builder for the current expression or block;
  • diagnostics for invalid lowering-time states.

Intrinsic handlers must not inspect raw AST, mutate arbitrary Sema state, emit backend-specific C, perform arbitrary host or filesystem access, or use global compiler singletons. Mutating compiler state is allowed only through declared effect classes and matching authority objects.

Intrinsic Effects and Diagnostics

Intrinsic effect and dependency classes are coarse implementation contracts used for context gating, cache validity, diagnostics, reflection, and tests. They are not a user-facing function effect system.

V1 intrinsic declarations may use these effect and dependency classes:

  • pure: depends only on explicit arguments and stable compiler-owned identities;
  • target_dependent: depends on selected target facts;
  • safety_dependent: depends on selected safety mode;
  • scope_dependent: depends on visible, current, or caller scope;
  • layout_dependent: depends on finalized layout facts;
  • conformance_dependent: depends on visible contract or conformance facts;
  • source_loading: may add module/include dependency edges;
  • attribute_registration: may mutate the attribute provider registry in a registration context;
  • attribute_mutation: may mutate the active attribute target;
  • diagnostic_effect: may emit Compiler.note, Compiler.warn, or Compiler.err during comptime evaluation.

lowering_expansion intrinsics may read target, safety, type, layout, and similar facts when declared, and may emit IR for the current lowered expression or block. They must not perform source loading, attribute registration, attribute mutation, user-requested diagnostic effects, or other compiler-state mutation.

Intrinsic failures fall into three buckets:

  • source diagnostics for invalid calls, invalid arguments, or invalid contexts;
  • deterministic comptime execution failures for Compiler.err, traps, resource exhaustion, or handler-reported comptime failure;
  • internal compiler errors for phase leaks, handler/metadata mismatches, ill-typed emitted IR, or compiler intrinsic calls that reach a phase where they are forbidden.

Intrinsic Examples

Primitive type factories are ordinary prelude wrappers over comptime_host intrinsics:

fn UnsignedInteger(comptime bits: comptime_int) Type {
  return Compiler.unsigned_integer_type(bits)
}

const u32 = UnsignedInteger(32)

The demanded comptime evaluation lowers the intrinsic call to a comptime host call and returns the canonical primitive Type handle:

Compiler.unsigned_integer_type(32)
-> comptime_host unsigned_integer_type(32)
-> TypeHandle(unsigned, 32)

Primitive arithmetic helpers are lowering_expansion intrinsics:

Compiler.int_add(u32, u32, a, b)

Lowering expands the call to ordinary IR:

Compiler.int_add(u32, u32, a, b)
-> checked_add u32 a b

Module and include wrappers are ordinary prelude functions over source-loading host intrinsics:

fn include(comptime path: []const u8) Namespace {
  return Compiler.include(path)
}

Demanding the wrapper may add a source-loading dependency edge and return a Namespace handle:

Compiler.include("./types.ct")
-> comptime_host include("./types.ct")
-> NamespaceHandle(...)

Attribute providers use host intrinsics with mutable target authority:

target.set_align(16)

During provider execution this dispatches through the active attribute-target authority and writes normalized metadata to that target. After the provider finishes, IR receives only finalized metadata, not raw setter calls.

Context Gating

Compiler intrinsics are public, but validity can depend on where evaluation is happening:

  • Compiler.err, Compiler.warn, and Compiler.note are valid only during comptime evaluation.
  • Compiler.module and Compiler.include are valid only where source-loading dependency edges may be added; their evaluated names or paths must satisfy the module/include rules.
  • Compiler.register_attribute_provider is valid only while evaluating an attribute declaration or registration context.
  • attribute target setters are valid only while an attribute provider is executing with a mutable target handle and before that target is frozen.
  • primitive type construction and target/safety queries are valid in comptime contexts and return comptime-only values.

Invalid context use is a hard compile error. For example, diagnostics should say that Compiler.include requires a declaration-scope source-loading context, or that Compiler.register_attribute_provider requires an active attribute-registration context. They should not describe the operation as private, privileged, or prelude-only.

Direct use of Compiler.* is valid source. Lints may suggest an idiomatic prelude wrapper only when the direct intrinsic call is exactly wrapper-equivalent and the suggestion does not imply the intrinsic is private or unstable.

Prelude Bootstrap

The prelude is source code evaluated under the same rules as user code. It starts in a compiler-seeded initial scope containing the minimal non-source identities needed to check the root prelude declarations:

  • Type;
  • bool;
  • void;
  • never;
  • comptime_int;
  • comptime_float;
  • the Compiler namespace and the public intrinsic declarations needed by the prelude root.

The prelude root file must contain enough source declarations to define primitive numeric factories and the primitive aliases needed by later prelude declarations without relying on include(...). In particular, the primitive factories take comptime_int, not u8, because u8 is one of the aliases being defined:

fn UnsignedInteger(comptime bits: comptime_int) Type {
  return Compiler.unsigned_integer_type(bits)
}

const u8 = UnsignedInteger(8)
const usize = UnsignedInteger(Compiler.target().pointer_width)

After u8 exists, string literals can have type []const u8, and the prelude can define ordinary include and module wrappers:

fn include(comptime path: []const u8) Namespace {
  return Compiler.include(path)
}

fn module(comptime name: []const u8) Namespace {
  return Compiler.module(name)
}

Included prelude files are ordinary source files checked through lazy top-level analysis after their source-loading declarations are demanded. They do not get a separate execution mode or hidden privilege.

Caching

Comptime caching is semantically invisible except for execution-time Compiler.note and Compiler.warn replay behavior documented in Comptime. V1 defines only per-compilation cache soundness. Cross-run incremental cache keys, loaded-input content identities, and fine-grained dependency invalidation are deferred to CEP-0061: Incremental Comptime Cache Invalidation.