cosmo0 Derive Macros

Status

This file owns the first cosmo0 derive macro design. It is intentionally narrower than general declaration macros and expression macros. The initial derive boundary only allows a provider to attach implementations of existing traits to an existing item. It does not introduce new public names, new members, new top-level declarations, or parser syntax.

Derive Boundary

A derive macro is selected from an attribute on an existing item:

@derive(cli.Parser)
class Config {
val package_name: String
}

The attribute identifies a derive provider. The target item already exists in the parsed declaration index before the provider runs. The provider receives compiler-selected reflection facts for that item and may return one or more trait implementation records for the same item.

Accepted first-slice output shape:

DeriveOutput:
impl trait cli.Parser for Config { ... }

Rejected first-slice output shapes:

new top-level function parse(...)
new static method Config.parse(...)
new field or variant on Config
new type alias or class
raw source text
trusted TypedExpr

This keeps derive expansion an implementation-attachment phase, not a declaration-introduction phase.

Name Resolution Boundary

Derive macros do not affect ordinary name resolution in the first slice.

The resolver builds the package/module declaration index and resolves ordinary names before derive output is needed. Because derive output can only attach an implementation of an already-known trait to an already-known item, expansion does not add a new binding that later source can refer to by name.

For example:

@derive(cli.Parser)
class Config { ... }

may make Config satisfy trait cli.Parser, but it does not create a visible parse name, a Config.parse member, or a new module declaration. Later code that uses parser behavior must do so through trait resolution or an explicit trait API that was already name-resolved:

val config = cli.Parser.parse[Config](args)

In this shape:

Provider Input

The compiler supplies stable derive input selected from parsed declarations and admitted type facts:

DeriveInput:
provider identity
source package identity
target item identity
target item kind
target item name, module path, visibility, and source span
admitted type parameters
fields, variants, defaults, attributes, and doc comments admitted by profile
selected trait identity
source spans and hygiene/origin metadata

The provider does not receive a mutable compiler module, a typed tree to patch, or a target runtime executable handle.

Provider Output

The provider returns serialized output:

DeriveOutput:
trait implementation records
consumed attributes
diagnostics
generated-source summary

Each implementation record must name:

The compiler validates the output before attaching it. Generated expression fragments inside implementation bodies are Expr[Untyped] and return to ordinary type checking.

Trait Implementation Attachment

Attaching a derive implementation is semantically equivalent to accepting an ordinary implementation block for the same trait and target, except that the implementation body is generated and carries derive origin metadata.

The attachment phase validates:

Because the output is an implementation attachment, not a new declaration, the ordinary declaration index is unchanged by derive expansion.

Trait Resolution Dependency

Derive output may affect trait resolution even though it does not affect ordinary name resolution. A generated implementation contributes an implementation fact:

ImplFact(trait = cli.Parser, target = Config)

The implementation fact index is built from both source impl declarations and derive-generated implementation attachments:

ImplFactIndex =
source impls
+ derive-generated impls

Type checking of an item that requires trait evidence depends on the relevant implementation fact. For example:

val config = cli.Parser.parse[Config](args)

uses ordinary name resolution for cli.Parser and Config, but it cannot finish trait resolution until ImplFact(cli.Parser for Config) is available. If a method-like trait API such as config.parse() is admitted, selector resolution must also wait for the method-set fact that can include derive-generated impls:

ResolveSelector(config.parse)
waits for TypeFact(config)
waits for MethodSetFact(Config, "parse")

The first derive slice keeps derive input smaller than this output dependency: derive expansion may depend on declaration header facts, selected trait identity, target item identity, fields, variants, attributes, defaults, doc comments, and admitted type facts. It must not depend on arbitrary body checking or trait-resolution facts unless a later inspector capability adds explicit dependency edges.

Examples

Accepted derive shape:

trait FieldCount {
def field_count(&self): i32
}

@derive(example.FieldCount)
class Config {
@arg(long = "package", short = "p")
val package_name: String

val verbose: Bool
}

The provider may attach an implementation equivalent to:

impl FieldCount for Config {
def field_count(&self): i32 = 2
}

Rejected first-slice output shape:

generated top-level def parse_config(...)
generated Config.parse(...)
generated field Config.cached_parser
trusted TypedExpr for a generated method body

Diagnostics

Derive diagnostics must point both to the derive attribute and to the target item fact that caused the issue. Generated implementation diagnostics must carry the generated span and the origin span from the derive input.

Examples:

cosmo0.derive.unresolved-provider
cosmo0.derive.unsupported-target
cosmo0.derive.unsupported-trait
cosmo0.derive.invalid-output
cosmo0.derive.duplicate-impl
cosmo0.derive.unconsumed-attribute

Non-Goals

The first derive macro slice does not provide:

Later declaration macro capabilities may introduce new names, but they must be specified separately because they do affect name resolution and declaration ordering.