Skip to content

Attribute Provider Model

Accepted

Accepted for V1 provider registration, target kinds, evaluation order, metadata mutation, and conflict handling.

An attribute provider is an ordinary comptime function marked by the prelude-defined @attribute attribute:

@attribute(.{ .targets = .{ .struct_type } })
fn resource(comptime target: Type, comptime context: Attribute.Context) void!Type.MetadataError {
  if !target.implements(Disposable, context.scope()) {
    Compiler.err(.{ .message = "@resource requires Disposable" })
  }

  try target.set_copyability(.forbidden)
}

@attribute itself is defined in the prelude, but it is the bootstrap attribute marker recognized by the compiler. When the compiler sees @attribute(...) on a function, it resolves attribute normally. If it is not available, compilation fails. The resolved declaration must be the compiler-provided prelude.attribute provider identity.

Compiler.register_attribute_provider(provider, spec)

@attribute may decorate functions only.

The registration API is a public compiler intrinsic, but it is context-gated. It is valid only while evaluating an attribute declaration or registration context; outside that context, the compiler rejects the call because no attribute-registration phase is active. User code normally creates attributes by declaring functions marked with @attribute(...); direct manual registration is valid only where that source form has established the registration context.

The attribute registration spec is:

const Spec = struct {
  targets: []const Attribute.TargetKind
  repeatable: bool = false
}

targets is required and non-empty. repeatable defaults to false.

The provider function shape is:

fn provider(
  comptime target: TargetType,
  comptime context: Attribute.Context,
  comptime arg1: Arg1,
  ...
) void

Shape snippets omit the @attribute(...) registration marker. Complete provider declarations must include it.

This provider shape is an attribute protocol, not the general capability-order convention from Functions. The decorated target comes first, the attribute evaluation context comes second, and user-supplied attribute arguments follow.

Attribute functions may also return errors:

fn provider(...) void!SomeError

The success return type must be void. Non-void success return values are invalid in V1 because attributes do not replace declarations or values. Unhandled provider errors become compile errors at the attribute use site.

Extra attribute arguments map to ordinary function parameters after context:

@deprecated("use new_name")
fn old() void

calls a provider shaped like:

@attribute(.{ .targets = .{ .function, .const_decl, .field, .contract_operation } })
fn deprecated(
  comptime target: Attribute.Target,
  comptime context: Attribute.Context,
  comptime message: ?[]const u8 = null,
) void

Attribute providers may be generic. No special generic attribute syntax is needed.

Attribute providers may have default parameters like ordinary comptime functions. V1 has no named attribute arguments, so user-supplied attribute arguments are positional only.

Provider shape is validated when the provider is registered:

  • the first parameter must be a comptime target parameter
  • the second parameter must be comptime context: Attribute.Context
  • all remaining parameters are attribute call arguments and must be comptime
  • the success return type must be void
  • an error return is allowed
  • the first parameter type must be compatible with every declared target kind in Attribute.Spec.targets

Target Kinds

Initial target kinds:

const TargetKind = enum {
  const_decl,
  function,
  function_type,
  impl_decl,

  field,
  param,

  struct_type,
  contract_type,

  contract_operation,
  impl_operation,
}

.function covers top-level functions and inherent methods. .function_type covers function type expressions such as @callconv(.c) fn(x: f32) f32. Contract operations and implementation operations stay separate because they have contract-specific semantics.

.const_decl covers ordinary const declarations and aliases. An attribute on an alias decorates the alias declaration, not the underlying type or value:

@deprecated("use NewFile")
const OldFile = File

.param covers runtime, destructured, and comptime/generic parameters. Parameter metadata exposes whether a parameter is comptime or runtime.

The first provider parameter may be a specific reflected target type when all declared target kinds are compatible:

@attribute(.{ .targets = .{ .struct_type } })
fn resource(comptime target: Type, comptime context: Attribute.Context) void {
}

For multi-target providers, use Attribute.Target, a comptime reflection handle with typed accessors:

@attribute(.{ .targets = .{ .field, .param } })
fn audit_tag(comptime target: Attribute.Target, comptime context: Attribute.Context) void!Attribute.TargetError {
  if target.kind() == .field {
    const field = try target.as_field()
  }

  if target.kind() == .param {
    const param = try target.as_param()
  }
}

The first parameter type must be compatible with all declared target kinds.

Attribute.Target is not a V1 discriminated union requiring pattern matching. It is a comptime-only reflection handle.

Attribute.TargetKind is a plain enum. Typed accessors such as as_type(), as_field(), as_param(), as_function_decl(), and as_function_type() report structured errors when the current target kind is incompatible.

Providers targeting multiple unrelated target kinds should use Attribute.Target. Providers targeting one semantic representation may use the focused reflected type directly.

Attribute Context

target is the decorated semantic entity. During attribute evaluation, it is a mutable finalization handle for metadata that belongs to that target. After attributes run, sema finalizes the target and freezes metadata controlled by those attributes.

context describes the decoration site:

const Context = struct {
  fn kind(self: Self) Attribute.TargetKind
  fn name(self: Self) ?[]const u8
  fn visibility(self: Self) ?Visibility
  fn scope(self: Self) Scope
  fn source(self: Self) ?SourceLocation

  fn enclosing_kind(self: Self) ?Attribute.TargetKind
  fn enclosing_name(self: Self) ?[]const u8
  fn enclosing_scope(self: Self) ?Scope
}

visibility() is nullable because not every target has its own visibility. For example, a type constructor in pub const File = @resource struct {} has no direct visibility; the surrounding const declaration does.

Source locations use the byte-span model from Type Reflection. Raw source text access is not special to attributes; if general comptime source-text APIs exist, attributes may use them under the same explicit host-access and determinism rules as other comptime code.

Parameter contexts expose limited enclosing metadata, such as enclosing kind, name, and scope. The containing function signature may not be fully frozen while parameter attributes run.

Metadata Mutation

Attributes mutate semantic metadata through explicit setter methods on the target. Setters may validate, return ordinary Catalyst errors, and record provenance for diagnostics:

try target.set_copyability(.forbidden)
try target.set_repr(.catalyst)
try target.set_alignment(16)
try target.set_calling_convention(.c)
try target.set_linkage(.export, .c)
try target.set_link_name("scale_f32")
target.attributes()

Setters are preferred over raw field assignment because they make side effects and validation visible. For example, set_copyability(.forbidden) is monotonic toward stricter copy rules; attempting to re-enable copyability after it was forbidden is invalid.

V1 attributes may mutate declared semantic metadata through explicit setters, but may not generate declarations, rewrite bodies, inject cleanup, or replace the target.

Attribute mutation is allowed for semantic input metadata.

Mutation boundaries:

  • long-term, if a change can be expressed through the general comptime type/declaration construction API, it should be possible here too.
  • derived compiler facts such as final layout size are not directly mutable.
  • mutate inputs such as representation, alignment, or fields through the appropriate construction/finalization APIs.

Copyability metadata is:

const Copyability = enum {
  copyable,
  forbidden,
}

Default is .copyable. Resource-owning types use set_copyability(.forbidden). There is no separate .resource copyability case; @resource is the standard attribute pattern that requires Disposable and forbids copying. Future Cloneable and Copyable contracts describe API capability, not this raw metadata enum.

Representation metadata is:

const Repr = enum {
  catalyst,
}

Default is .catalyst. Prelude Attributes owns accepted @repr cases and deferred representation modes.

Alignment metadata is set with set_alignment(bytes). On a struct_type, alignment is the minimum aggregate alignment. On a field, alignment is the minimum field alignment. Layout Reflection owns final layout and alignment validation.

Calling-convention metadata belongs to function signatures. Linkage metadata belongs to external symbol binding. Providers such as @export(.c) that imply a call ABI must set or validate the function signature calling convention; they must not store a second call-convention fact in linkage metadata.

Every successfully applied attribute is stored as provenance metadata on the direct target:

target.attributes()

Stored provenance includes:

  • resolved provider identity
  • argument source/provenance
  • source location
  • target kind

V1 accepted attribute arguments have no reflected argument names because named arguments are deferred to CEP-0015: Named Arguments. Optional normalized source text may be present for display and documentation.

V1 semantic reflection does not expose raw normalized attribute argument values. Semantic facts produced by attributes are read through focused target metadata APIs such as representation, alignment, copyability, calling convention, and linkage.

Provenance-only attributes such as @deprecated remain lintable by inspecting accepted attribute provenance. They do not create compiler-owned deprecation metadata. This applies to passive attributes such as @deprecated("use NewName") and semantic mutation attributes such as @resource.

Failed, unknown, or invalid attributes are not exposed through semantic reflection. Providers that emit notes or warnings and then complete successfully are still accepted and reflected. Parser and AST tooling may still inspect raw syntax.

Attribute provenance preserves source/evaluation order for attributes directly applied to the same target. Provenance does not inherit or propagate across aliases, containing declarations, type constructors, fields, parameters, contract operations, or implementation operations.

A provider may change focused semantic facts through explicit setters, but the recorded attribute provenance remains attached to the direct target.

Evaluation Order

Type-constructor attributes run after the type body and requirements have enough semantic information for reflection, but before the Type is finalized:

  1. Parse type constructor.
  2. Build provisional Type.
  3. Analyze members.
  4. Resolve struct(...) or contract(...) requirements enough for reflection.
  5. Run type-constructor attributes in source order.
  6. Finalize and freeze final Type metadata.
  7. Expose final Type to surrounding code.

Declaration attributes run after the declaration header is resolved and after child metadata needed by the declaration is finalized:

  1. Parse declaration.
  2. Resolve header, signature, visibility, and child parameter metadata.
  3. Run declaration attributes in source order.
  4. Finalize and freeze metadata controlled by those attributes.
  5. Check the body using final metadata.
  6. Export public API using final metadata.

Parameter attributes run while resolving the containing signature:

  1. Parse parameter.
  2. Resolve parameter name, type, and modifiers.
  3. Run parameter attributes in source order.
  4. Finalize and freeze parameter metadata.
  5. Build final containing signature.

Function declaration attributes can inspect final parameter metadata. Parameter attributes can inspect their own resolved type/name and limited enclosing declaration metadata.

Function type attributes run after the function signature is resolved and before the function type is used as a completed type:

  1. Parse function type expression.
  2. Resolve parameter and result types.
  3. Run function type attributes in source order.
  4. Finalize and freeze signature metadata.
  5. Expose the completed function type.

Field attributes run after the field name and type are resolved and before struct layout finalization.

Implementation and contract operation attributes run after the operation signature is resolved and before operation metadata participates in lookup or conformance checking.

Finalization is a sema-owned target lifecycle step, not a provider callback. Providers run once in source order; setters store semantic input metadata and provenance; sema runs target-specific cross-attribute validation; metadata freezes; later phases read finalized metadata.

Generic type-constructor attributes evaluate for each produced type:

fn Buffer(comptime T: Type) => @resource struct(Disposable) {
}

The compiler may cache deterministic results for identical instantiations, but semantically attributes run per produced Type.

Provider Resolution

Attribute names resolve through normal lexical/import visibility rules. There is no global magic registry and no separate attribute import mechanism. The resolved declaration must be a function registered with @attribute(...).

The prelude initial scope makes unqualified prelude attributes such as @resource visible. The compiler-provided prelude namespace also allows qualified spelling such as @prelude.resource. User-defined providers may be used through imported names or namespace-qualified paths such as @my_attrs.range(...).

Tooling can discover known attributes by finding functions marked with @attribute(...) and reading their declared target metadata. That metadata is also used by sema to reject impossible target/provider combinations during provider resolution.

V1 does not support overloaded attribute providers; duplicate visible provider names are rejected. Once CEP-0004: Function Overloading exists, target-kind metadata may filter compatible providers before ordinary overload resolution chooses the function.

Availability of host resources to comptime attribute code is governed by the general comptime resource policy.

Repetition and Conflicts

@attribute provider metadata declares whether an attribute is repeatable:

@attribute(.{
  .targets = .{ .function, .field },
  .repeatable = true,
})
fn audit_tag(...) void {
}

Default is repeatable = false. Duplicate checks use resolved provider identity, not spelling.

The compiler rejects these cases before provider execution:

  • an attribute name cannot be resolved
  • the resolved declaration is not a registered attribute provider
  • the provider target list does not include the actual target kind
  • a non-repeatable provider appears more than once on the same target
  • attribute argument arity, type, or comptime checks fail
  • an unsupported target site is used, such as a local variable, expression, statement, call argument, or arbitrary type-use site

During provider execution, Compiler.err, an unhandled provider error, or a failing setter validation rejects the attribute use as a hard compile error at the attribute use site unless a more precise source location is supplied. Lints own redundant but valid combinations, order-dependence warnings, and policy diagnostics that must be stable independent of comptime caching.

Conflict ownership

There is no generic mutual-exclusion mechanism in V1. Providers validate conflicts they own, target setters reject invalid state transitions, and lints warn about suspicious combinations.

Attributes do not automatically inherit or propagate. They apply only to their direct target unless the provider explicitly changes other metadata through normal APIs.