← Catalog

RFC-010AcceptedLanguage

Plugin & Macro System

GitHub

Depends: RFC-001RFC-009

Blocks: RFC-004RFC-007RFC-011

1. Abstract

This RFC defines Aura’s macro and compiler plugin story: hygienic token-tree expansion, attribute derives, and an out-of-process procedural macro protocol with sandboxing, lockfile checksums, and a versioned plugin ABI. The compiler keeps unsupported or unsandboxed plugin execution fail-closed.

2. Motivation

2.1 Problem statement

Users need derives (Equals, Debug), test registration, and boilerplate reduction. Unrestricted in-process plugins are a supply-chain nightmare; no macros at all force painful codegen external to the language.

2.2 Why now

Compiler expansion points (RFC-004) and attributes (RFC-009) need a concrete macro model.

2.3 Success metrics

MetricTarget
MVP derivesEquals/Hash/Debug work
HygieneNo accidental capture of user locals
SafetyProc plugins cannot read arbitrary FS by default

3. Goals

  • Declarative macros + derive attributes in MVP.
  • Hygienic expansion by default.
  • Clear phase ordering with typecheck.
  • Path to sandboxed procedural macros.

4. Non-goals

  • Arbitrary compiler modification plugins in MVP.
  • Unhygienic text-paste macros as the default.
  • Full Template Haskell power day one.

5. Prior art & alternatives

SystemNotesTake
Rust macrosdecl + proc, hygienePrimary inspiration
Lisp macrosPowerfulToo free-form
Java APTBuild-time processorsPhase inspiration
C macrosTextualReject

6. Design

6.1 Overview

FeatureMVPLater
Declarative macro! / macro_rules-styleYes
@derive(TraitLike)Yes
Attribute macros (custom)Limited builtinsUser proc
Proc macros (Rust dylib / WASM sandbox)NoYes
In-process arbitrary pluginsNoMaybe never

6.2 Declarative macros

aura
macro! vec {
  () => { Vec.new() };
  ($($x:expr),* $(,)?) => { /* ... */ };
}
  • Pattern-based; expands to tokens/AST.
  • Hygienic identifiers by default; explicit span/unhygienic opt-in rare.
  • Invoked as vec!(1, 2, 3) or function-like form; exact grammar amended before implement (derives ship first).
  • Packaging: macros live in normal packages (no separate macro package kind); consumers depend like any library.
  • Exported declarative macro names are unique within a resolved package graph; the loader rejects duplicate names before expansion so dependency order cannot change which definition runs. Plugin executable provenance remains tied to the root package manifest and is never inherited implicitly from dependencies. A dependency manifest that declares [macro_plugins] is rejected during graph loading rather than silently dropping that executable declaration; root plugin paths must stay package-relative without parent traversal, and root plugin executables are pinned by macro_plugin.<Name> lock entries with SHA-256 checksums. Dependency plugins require a future provenance policy.
  • Template-introduced bindings and item names receive an invocation-local gensym resolved through lexical token-tree scopes, including nested blocks and function parameters; shadowed bindings therefore remain distinct. Identifiers supplied through metavariables retain caller spelling. Explicit explicit unhygienic spans and expansion inspection remain future surface area; generated template tokens are attributed to the invocation span.

6.3 Derive macros

The alpha derive vocabulary is Debug, Equals, Hash, ToString, and Json, with generated-member and ownership rules recorded in docs/api/compiler-alpha.md. The names are locked before expansion is implemented.

aura
@derive(Debug, Equals)
class Point(val x: Int, val y: Int)
  • Built-in derives implemented in compiler or std plugins.
  • Generate members: equals, hashCode, toString/debugString.
  • User derives later via proc macros implementing a stable interface.

6.4 Expansion order

  1. Parse AST.
  2. Collect attributes & macro invocations.
  3. Expand outer-to-inner with recursion limit.
  4. Re-resolve names after expansion.
  5. Typecheck (some derives may need partial types—phased expansion allowed for advanced derives later).

6.5 Procedural macros (phase 2)

  • Implemented as a separate sandboxed process receiving serialized AST and returning generated items. Unsupported hosts, malformed responses, timeouts, output-limit violations, and non-zero exits fail closed with diagnostics.
  • Host language for authoring plugins: Rust first (matches toolchain).
  • Capabilities: no network; FS limited to crate source root if needed; CPU/time limits.
  • Distributed as versioned packages (RFC-005) with checksums.

The current ABI is versioned and validates request/response framing. Root plugins are checksum-pinned in the lockfile; dependency-owned plugin declarations are rejected until a provenance policy exists.

6.6 Error model

CaseBehavior
Expand errorSpan at invocation + macro definition notes
Infinite recursionHard limit error
Plugin crashCompile error, not host ICE when sandboxed
Unresolved after expandNormal name resolution errors

6.7 Examples

aura
@derive(Debug, Equals)
class User(val id: Long, val name: String)

fun demo() {
  val u = User(1, "a")
  println(debug(u))
}

6.8 Compatibility & migration

  • Built-in derive names reserved.
  • Proc macro ABI versioned; old plugins rejected with upgrade message.
  • Editions may change hygiene edge cases carefully.

7. Open questions

#QuestionOptionsOwnerStatus
1Declarative syntax exact formLangResolved — derives first; decl form = hygienic pattern macros (syntax amend pre-impl)
2Proc sandbox: WASM vs processseparate process firstToolchainResolved
3Macro packaging unitPkgResolved — normal packages export macros; no separate macro package kind

8. Rationale & trade-offs

MVP without full proc macros reduces security and stability risk while covering 80% of boilerplate via derives and decl macros. Sandboxing later enables ecosystem power without making the compiler a plugin malware host. Cost: fewer magical frameworks early—aligned with core-only scope.

9. Unresolved / future work

  • Full macro debugging tools (cargo expand equivalent)
  • IDE expansion preview
  • Official derive set list

10. Security & safety considerations

  • Treat proc macros as untrusted code execution at compile time.
  • Lockfile + checksums for plugin packages and root procedural executables.
  • Capability denial by default (network/FS).
  • No ambient unsafe injection into user code without unsafe in expansion output still gated by user’s allowance—expanded unsafe should be visible (macro expand audit).

11. Implementation plan (optional)

PhaseScopeExit criteria
P0Built-in derivesEquals/Debug
P1Declarative macrosvec!-like
P2Sandboxed procCustom derive demo

12. References

12.1 Implemented host derive boundary

The Rust compiler exposes aura_sema::UserDerive/aura_sema::UserMacro and check_file_with_derives/check_file_with_macros. Registered implementations receive an AST class or file and expand before typecheck; duplicate members, ownership rules, diagnostic phase markers, and expansion-origin metadata are applied uniformly with built-in derives. These host APIs are deliberately not a package-level proc-macro ABI: source-level token macros and untrusted procedural derives still require the RFC-010 process sandbox and versioned plugin protocol.

The process protocol is exposed as encode_plugin_request, decode_plugin_request, and encode_plugin_response. On Linux the host uses bubblewrap with network isolation, read-only source/plugin/runtime mounts, and a private temporary directory; hosts without a supported sandbox fail closed. Every UTF-8 field is capped at 16 MiB and oversized fields are rejected before plugin execution or decoder allocation.


Changelog

DateAuthorChange
2026-08-02Add fail-closed 16 MiB field limits to the stable plugin protocol
2026-08-01Implement Linux bubblewrap sandbox and export the versioned request encoder
2026-07-16Lock derives-first + package-export macros; Status → Accepted
2026-07-16Status → In Review — Review: derives MVP + process sandbox locked; declarative syntax open
2026-07-15Initial skeleton
2026-07-15Solid draft: derives MVP, sandboxed proc later
2026-07-15Lock process sandbox for proc macros