Skip to content

16 · Reflection & Metaprogramming

Custom Derive

Register your own derive with @derive_for: a comptime fn receives typeinfo and returns Zolo source to splice.

On this page

@derive_for(Trait) registers a comptime function as the code generator for that trait. The function receives typeinfo(Self) and returns a Zolo source string — either an impl Trait for Self or an inherent impl Self { ... } — which the compiler splices next to the type definition. No compiler modification required.

Register a FieldList derive with @derive_for, generate an inherent impl that joins field names as a comma-separated string, and verify a.field_list() returns "id,owner,balance".

12-derive-custom.zolo
Playground
// Feature: define your OWN derive. `@derive_for(Trait)` registers a comptime

// function that receives typeinfo(Self) and returns structured Zolo syntax,

// spliced next to the type. The result can be `impl Trait for Self` OR — when

// you just want a method — an INHERENT `impl Self { ... }`.


@derive_for(FieldList)
fn derive_field_list(info) -> Syntax<.Items> {
  let names = info.fields.map(|field| field.name).join(",")
  // Inherent impl: adds `field_list` directly onto the type — no `trait`

  // declaration needed, and it coexists with any hand-written `impl` blocks.

  return quote items {
    impl ${info.ref} {
      fn field_list(self) -> str { return ${names} }
    }
  }
}

@derive(FieldList)
struct Account {
  id: int,
  owner: str,
  balance: int,
}

let a = Account.new(id: 1, owner: "x", balance: 0)
print(a.field_list())
// expected: id,owner,balance

Refer to types and fields

Typed attribute schemas can use TypeRef and FieldRef to accept symbols: @relation(target: Team, key: Team.code, action: .Cascade). The derive receives the resolved declaration, its reusable syntax and its canonical identity. Imported aliases retain that identity; a field from another type can be rejected without comparing textual names.

Generate a method that reads a referenced field, validate its owner at compile time, and use an enum option with contextual shorthand.

10-typed-attribute-references.zolo
Playground
//! Typed derive options can refer to declarations and fields without strings.

enum RelationAction { NoAction, Cascade }

struct RelationOptions {
  target: TypeRef?,
  key: FieldRef?,
  action: RelationAction = .NoAction,
}

@derive_for(Relationship, namespace: relation, field_attrs: RelationOptions)
pub fn Relationship(info) -> Syntax<.Items> {
  let options = info.fields[0].opts
  let target = options.target
  let key = options.key
  if target == nil || key == nil { panic("target and key are required") }
  if target.identity != key.owner.identity { panic("key belongs to a different type") }
  let label = target.info.name + "." + key.name
  let cascades = options.action == RelationAction::Cascade
  return quote items {
    impl ${info.ref} {
      fn relation_name() -> str { ${label} }
      fn cascades() -> bool { ${cascades} }
      fn related_key(parent: ${target.ref}) -> ${key.info.type.ref} {
        parent.${key.member}
      }
    }
  }
}

@derive(Relationship)
struct Member {
  @relation(target: Team, key: Team.code, action: .Cascade)
  team_code: str,
}

// Forward references are resolved as declarations during compilation.

struct Team { code: str }

assert(Member::relation_name() == "Team.code")
assert(Member::cascades())
assert(Member::related_key(Team.new("engineering")) == "engineering")
print(Member::relation_name())
print(Member::related_key(Team.new("engineering")))

Collect typed field references

Typed options also accept nested collections. A schema such as indexes: #{str: [FieldRef]} lets the caller write @index(indexes: #{tenant_email: [Account.tenant_id, Account.email]}). Each leaf is checked as a field reference. The map label stays a name; field order is preserved.

Compare field.owner.identity with the derive target's info.identity to reject fields from another declaration, including a same-name type in another module. Imported aliases of one declaration share the identity.

Declare a composite index as field references, validate their owners and generate a method listing the fields in the authored order.

11-nested-typed-attribute-references.zolo
Playground
//! Collection option schemas resolve every nested declaration reference.

struct IndexOptions {
  indexes: #{str: [FieldRef]} = #{},
}

@derive_for(Indexed, namespace: index, type_attrs: IndexOptions)
pub fn Indexed(info) -> Syntax<.Items> {
  let fields = info.opts.indexes["tenant_email"]
  if fields.len() == 0 { panic("index must contain a field") }
  let names: [str] = []
  for field in fields {
    if field.owner.identity != info.identity {
      panic("index field must belong to the annotated model")
    }
    names.push(field.name)
  }
  let label = names.join(", ")
  return quote items {
    impl ${info.ref} {
      fn index_fields() -> str { ${label} }
    }
  }
}

@derive(Indexed)
@index(indexes: #{tenant_email: [Account.tenant_id, Account.email]})
struct Account {
  tenant_id: int,
  email: str,
}

assert(Account::index_fields() == "tenant_id, email")
print(Account::index_fields())

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

en