Dynamic Contract Metadata¶
Accepted
Accepted for the V1 dynamic contract metadata construction and descriptor APIs.
Metadata API¶
Ordinary comptime code can request compiler-checked dynamic contract metadata for a concrete implementer and a dyn-safe semantic contract surface:
I.dyn_metadata(C, scope: ?Scope = null) Type.DynMetadata!Type.DynMetadataError
The semantic requirement is fixed: this is a general reflection primitive for forming dynamic contract objects, not a Box special case and not restricted to trusted prelude code.
I must be a concrete sized implementer type. C must be a fully applied dyn-safe semantic contract constraint, possibly a normalized intersection. C is not written as dyn C and cannot be a structural constraint from satisfies(...). Lookup is visibility-aware and uses the supplied scope. When the optional scope argument is null, the called API resolves it to Scope.caller() in its body before delegating.
dyn_metadata is a construction-metadata API. It returns Type.DynMetadataError when C is not dyn-safe; it does not return partial vtable metadata plus diagnostics. Tooling that needs structured dyn-safety diagnostics should call C.dyn_safety() before requesting construction metadata.
Dyn-safety is necessary but not sufficient for metadata construction. I.dyn_metadata(C, scope) also requires I to visibly implement C in the supplied lookup scope. C.dyn_safety().ok may be true while I.dyn_metadata(C, scope) still fails because the conformance is missing, ambiguous, or not visible from that scope.
For a normalized contract intersection such as A & B, metadata construction follows conformance reflection: I must visibly implement every normalized component in the supplied scope. If any component conformance is missing, ambiguous, or not visible from that scope, metadata construction fails for the whole intersection. An explicit implementation of the intersection itself is not required. Dynamic intersection metadata assembles the needed component vtables only from visible component conformances.
Construction Errors¶
Type.DynMetadataError owns the construction failure shape. It may include nested conformance or dyn-safety details, but it is not just an alias for Type.ConformanceLookupError:
const DynMetadataErrorKind = enum {
invalid_implementer,
invalid_contract,
contract_not_dyn_safe,
conformance_lookup,
}
const DynMetadataError = struct {
kind: Type.DynMetadataErrorKind
conformance_error: ?Type.ConformanceLookupError
dyn_safety: ?Type.DynSafety
source: ?SourceLocation
message: ?[]const u8
}
conformance_error is present when kind = .conformance_lookup. dyn_safety may be present when kind = .contract_not_dyn_safe. Tooling should branch on kind and use nested metadata for details rather than treating all metadata-construction failures as conformance lookup failures.
invalid_contract is the coarse wrong-target error for construction metadata. It covers inputs that are not fully applied semantic contract constraints, such as structural constraints, mixed structural/semantic intersections, not-fully-applied contract factories, or already-erased dyn C types. Tooling that needs finer classification should use type classifiers and C.dyn_safety() rather than expecting a separate DynMetadataErrorKind for every wrong contract shape.
invalid_implementer is the coarse wrong-target error for the payload type. V1 metadata construction requires a concrete sized implementer type. Non-concrete types, constraint types, and concrete unsized types are invalid implementers. Supporting unsized implementers would require separate payload layout and pointer-formation rules and is deferred.
Box(dyn D) is not a special payload-erasure implementer for D's hidden payload. Calling Box(dyn D).dyn_metadata(C) would request metadata for the box owner value itself, using only ordinary visible conformances of Box(dyn D). Dyn-to-dyn reshaping of an already erased box uses represented metadata operations such as (move Box(dyn D)).upcast(C), not fresh payload-erasure metadata construction. Closed prelude forwarding implementations for selected boxed erased types remain ordinary conformances of the box owner type.
Returned Metadata¶
The returned metadata describes the dynamic surface and payload disposal behavior needed by owner and pointer abstractions:
const DynMetadata = struct {
implementer: Type
contract: Type
pointer_metadata: Type.DynPointerMetadata
dispose: Type.DynDisposeMetadata
}
pointer_metadata is the compiler-produced metadata needed to form *dyn C / *const dyn C fat pointers for values of type I.
Pointer metadata rules:
- it includes every vtable pointer required by normalized intersections.
- it includes only active dynamic operations in the checked dyn-safe surface.
- it includes, or can deterministically derive at compile/lowering time, every descriptor needed for valid dependency upcasts and intersection subset upcasts from the represented surface.
- it does not expose a public list of supported upcast targets in V1; tooling derives possible upcasts from the represented contract type and contract composition reflection.
- static-only guarded operations are not stored in
Type.DynMetadata; use contract reflection or dyn-safety reflection for tooling and diagnostics that need to inspect operations absent from the dynamic surface. - user and prelude code should treat vtable construction as compiler-owned.
- metadata values may be stored, passed, and embedded.
- vtable and dispose components are not directly aggregate-constructible or forgeable by user code.
dispose describes how an owning abstraction ends the original concrete payload lifetime.
Dispose metadata rules:
- if
IimplementsDisposable, disposal calls that implementation'sdisposefor the payload. - if
Idoes not implementDisposable, disposal is a no-op. - lookup is based on
I'sDisposableconformance, not on whether the erased contract surfaceCincludesDisposable. - cleanup must remain correct after dynamic upcasts and intersection narrowing.
- dispose metadata does not encode which allocator or storage-free strategy to use.
- allocation ownership belongs to the owner type that allocated the storage, such as
Box. - owner types store allocator/freeing state separately.
- dispose metadata is separate from the visible contract vtable because cleanup is an owner responsibility, not a dynamically exposed method requirement.
Representation Boundaries¶
V1 dynamic metadata does not include public runtime concrete type identity. Runtime type identity, downcasting, and sidecasting are deferred to CEP-0006: Runtime Type Identity and Dyn Casts.
Type.DynMetadata and its nested metadata values are comptime-only reflection values, like Type itself. They are not runtime field types and cannot appear directly in ordinary runtime structs. They may expose or produce runtime-storable descriptor fields/constants. Ordinary owner types such as Box(dyn C) or future user-defined dyn-owning containers store those derived runtime descriptor fields, not the Type.DynMetadata reflection object.
The concrete representation of Type.DynPointerMetadata and Type.DynDisposeMetadata is intentionally not a public ABI in V1. They are deterministic comptime metadata values whose runtime components can be embedded into generated code or stored by ordinary owner types.
Descriptor APIs¶
Forming borrowed dynamic pointers uses methods on compiler-produced metadata or descriptor objects, not free functions and not manually assembled fat-pointer fields:
const meta = try I.dyn_metadata(C)
var p: *dyn C = meta.ptr(&value)
var p_const: *const dyn C = meta.ptr_const(&value)
const pointer_desc = meta.pointer_descriptor()
var owner_p: *dyn C = pointer_desc.ptr(data)
var owner_p_const: *const dyn C = pointer_desc.ptr_const(data)
meta.ptr and meta.ptr_const accept typed data pointers only (*I or *const I). This lets the compiler verify that the data pointer type matches the implementer type used to produce the metadata. They combine the data pointer with compiler-produced pointer metadata and return the appropriate borrowed dynamic fat pointer. They do not transfer ownership and do not affect dispose metadata.
pointer_descriptor() is a comptime operation that returns an unforgeable runtime-storable descriptor derived from the metadata.
Pointer descriptor rules:
- it produces descriptor constants or fields.
- it does not allocate or build descriptor state at runtime.
- it must not discover, synthesize, or rebuild dependency or component descriptors at runtime for valid dynamic upcasts.
- owner types store this descriptor beside their erased data pointer.
- later,
pointer_desc.ptr(data)andpointer_desc.ptr_const(data)borrow from already-validated owner storage. - the descriptor method may accept erased data internally.
- the descriptor itself is compiler-produced and not directly aggregate-constructible, so ordinary code cannot pair arbitrary opaque data with unrelated vtables.
- lowering may use erased pointers internally.
- safe public construction from a concrete value remains typed through
meta.ptr/meta.ptr_const.
dispose_descriptor() follows the same model. It is a comptime operation that returns an unforgeable runtime-storable descriptor derived from the metadata. It produces descriptor constants or fields; it does not allocate or build descriptor state at runtime. Owner types store this descriptor beside their erased data pointer and allocator/freeing state. Later, the owner invokes the descriptor to end the payload lifetime:
self.dispose_desc.run(self.data)
self.allocator.free(self.data)
The dispose descriptor destroys the payload only. It does not free storage and does not encode allocator policy. In safe Catalyst source it is an unforgeable descriptor with a method, not a raw public fn(*opaque) void value. It must lower to at most a null check plus an indirect function call:
The compiler may optimize it to no code for known non-Disposable payloads, or to a direct/inlined Disposable.dispose call when the concrete type remains statically known.