Skip to main content

decorator

decorator declares a multi-phase metaprogramming transform that can wrap, constrain, and generate code for any declaration it annotates. A decorator definition bundles up to four phases — compile, typecheck, runtime, emit — and is applied concisely with the @name symbol form. Decorators are the backbone of AdeshLang's compile-time metaprogramming.

Ground truth: Lexer TokenKind::Decorator (src/parsing/lexer.rs:1004, keyword "decorator"), TokenKind::At (lexer.rs:400, "@"), StmtKind::Decorator(DecoratorDef, bool) (src/parsing/ast.rs:687), DecoratorDef { name, params, phases, requires_unsafe, is_new_style } (ast.rs:711-718), DecoratorPhase::{Compile,Runtime,Typecheck,Emit}(Arc<Vec<Stmt>>) (ast.rs:698-708), DecoratorPipeline/DecoratorPipelineStage (ast.rs:720-736), applied via @decorator / @decorator(args).


Syntax

decorator_decl ::= "decorator" ident "(" param_list? ")" decorator_body
decorator_body ::= "{" phase* "}"
phase ::= "compile" "{" stmt* "}"
| "typecheck" "{" stmt* "}"
| "runtime" "{" stmt* "}"
| "emit" "{" stmt* "}"
param_list ::= param ("," param)*
param ::= ident (":" type)? ("=" expr)?

decorator_apply ::= "@" ident ("(" arg_list? ")")?
applied_decl ::= decorator_apply+ ( "fn" ... | "class" ... | "struct" ... | "let" ... )

decorator is reserved (TokenKind::Decorator). @ is a separate token (TokenKind::At) consumed by the parser's decorator-attachment loop (src/parsing/parser/declarations.rs:111).

Defining a decorator — all four phases (rare)

decorator audited(target) {
compile { emit(`target.__audited = true;`); }
typecheck { require target.params.len() == 1, "audited expects single param"; }
runtime { print("audit: " + target.name); return call.proceed(); }
emit { directive("audit", target); }
}

Most decorators use one or two phases; four-phase decorators are compiler-plugin-like and uncommon.

Applying a decorator

@logged
@retry(3, 100)
fn fetch(url: string): string { return http_get(url); }

// Decorator with args is a Call-like syntax after @ — args captured as DecoratorPipelineStage.args
// Zero-arg form @logged and parameterized @retry(3) are both valid.

Parser loop: while matchk(&[TokenKind::At]) { parse decorator name + optional (args) } (declarations.rs:111-127), decorating the immediately following declaration.


Semantics

Compilation model — how decorators compile and attach

decorator decl applied declaration
Source ──► Lex ──► "decorator" ident(params) "{" phases "}" ──► @name fn f() { ... }
TokenKind::Decorator TokenKind::At
│ │
▼ ▼
DecoratorDef { DecoratorPipeline
name, params, { fn_id, stages, pipeline_hash }
phases: Vec<DecoratorPhase> stages: Vec<DecoratorPipelineStage>
requires_unsafe, each = { decorator_name, phase, args }
is_new_style hash = fnv(phases + order)
} (stored in registry) │
│ │
└────────────────────┬───────────────────────┘

HIR build + phase execution
┌──────────────────────────────────────┐
│ 1. compile{} phases (in def order) │
│ → AST mutation via emit │
│ 2. HIR rebuild │
│ 3. typecheck{} phases │
│ 4. Lower → LIR → emit{} phases │
│ 5. Synthesize runtime wrappers │
│ call.proceed() chain │
└──────────────────────────────────────┘


Runtime calls
@a @b fn f() means a(runtime(b(runtime(f))))
  1. Lex/Parsedecorator name(params) { ... }DecoratorDef with phases: Vec<DecoratorPhase> (ast.rs:711). Each phase body is Arc<Vec<Stmt>>.
  2. RegistryDecoratorDef is stored in decorator_registry.rs; requires_unsafe is inferred if any phase contains unsafe.
  3. Application@name / @name(args) before a decl pushes DecoratorPipelineStage { decorator_name, phase, args: Vec<Expr> } (ast.rs:722-727). @logged (no parens) yields args: []; @retry(3) yields args: [3]. Order matters: @a @b fn f()[a_stage, b_stage, ...] where a is outermost wrapper.
  4. Pipeline hashDecoratorPipeline.pipeline_hash: u64 (ast.rs:734-735) caches compiled wrappers; changing phase order/args changes the hash.
  5. Phase execution — see table below. After compile{} mutations, HIR is rebuilt before typecheck{}/emit{}/runtime.
  6. Runtime synthesis — each runtime{} becomes a wrapper function; call.proceed() (TokenKind::Proceed) lowers to the next wrapper or the original body.

Phase cheat-sheet

PhaseTokenKind (ast.rs:2072-2076)Whencall.proceed()emit?Sees call?Typical use
compileCompilecompile, before typecheckNoYes (AST injection)NoAST rewrite, metadata
typecheckTypechecktype resolutionNoNoNo (sees target)Type constraints
runtimeRuntimeper-call at runtimeYes (Proceed)NoYes (call.args)Logging, retry, cache
emitEmitLIR loweringNoIR onlyNoIR/backend directives

A decorator may omit any phase; omitting runtime means the decorated declaration runs undecorated at call time (pure compile-time decorator).

Arguments and is_new_style

  • decorator with_args(n: i32, label: string = "x") { ... } — params behave like function params (with optional type and default, ast.rs:713).
  • @with_args(5) / @with_args(5, "hi") — positional args matched to params; missing defaulted params use their defaults.
  • is_new_style: bool (DecoratorDef::is_new_style) tracks whether the definition used the newer multi-phase block syntax vs. legacy single-body form.

unsafe decorators

If any phase contains an unsafe block, DecoratorDef.requires_unsafe becomes true and decorating an unsafe fn without unsafe at the application site is an error (see unsafe keyword docs). This mirrors Function.is_unsafe.

Stacking and composition

@a @b @c fn f() {}
// Desugars to pipeline [a_stage, b_stage, c_stage]; call chain:
// a.runtime { call.proceed() → b.runtime { call.proceed() → c.runtime { call.proceed() → f } } }

compile phases all fire before any runtime wrapper synthesis, so a's compile sees the pre-decoration AST; interleaving is not per-decorator round-robin.


Examples

Example 1 — Simple logging decorator (single runtime phase)

// The most common shape: one runtime phase that wraps every call.

decorator logged(target) {
runtime {
let t0 = now_ms();
print("→ " + target.name + "(" + call.args.join(", ") + ")");
let result = call.proceed(); // delegate exactly once
let dt = now_ms() - t0;
print("← " + target.name + " = " + result + " (" + dt + "ms)");
return result;
}
}

@logged
fn add(a: i32, b: i32): i32 { return a + b; }

@logged
fn greet(name: string): string { return "hi " + name; }

print(add(2, 3)); // → add(2, 3) ← add = 5 (…ms) → 5
print(greet("Adesh")); // → greet(Adesh) … → greet = hi Adesh → hi Adesh

// Key rule: omit `return` before proceed and caller sees null.
// Fixed inside decorator: always `return call.proceed();` unless intentionally short-circuiting.

Example 2 — Multi-phase decorator with args, validation, and emission

// Caching decorator with compile-time emission (per-site cache), type checking, and runtime interception.

type Numeric = i32 | i64 | f64;

decorator memoize(target) {
compile {
// Validate shape at decoration site (AST phase)
require target.params.len() == 1, "memoize expects single-arg function";
// Emit a dedicated cache per decorated function — hygienic name includes target.name
emit(`let __memo_${target.name}: Map<string, any> = Map.new();`);
}
typecheck {
// Param should be hashable; return can be any — keep constraint light
// Require that the single param is not void
require target.params[0].type != "void", "memoize param must not be void";
}
runtime {
let key = call.args[0].to_string(); // simplistic key; real impl handles objects
let cache = lookup_memo_cache(target.name); // finds emitted __memo_<name>
if (cache.has(key)) {
print("cache hit for " + target.name + " key=" + key);
return cache.get(key); // short-circuit: no proceed
}
let value = call.proceed(); // cache miss — compute
cache.set(key, value);
return value;
}
}

decorator retry(n: i32) {
compile { require n > 0 && n <= 5, "retry attempts must be 1..5"; }
typecheck { require n is i32; }
runtime {
let last: any = null;
for i in 0..n {
try { return call.proceed(); }
catch e { last = e; print("retry " + (i + 1) + "/" + n + " failed: " + e); }
}
throw last;
}
}

// Stacking: memoize outermost, retry inner — retries only happen on cache miss
@memoize
@retry(3)
fn expensive(n: i32): i32 {
if (rand() < 0.4) { throw "transient failure"; }
return n * n;
}

print(expensive(7)); // may retry, then caches
print(expensive(7)); // cache hit → no retry needed
print(expensive(8)); // new key → recompute with retry

// Decorator with arguments: retry's param n is bound from @retry(3)
// and visible as `n` inside compile/typecheck/runtime blocks

Example 3 — Class decorator, @ stacking order, and emit backend hint

// Class decorator (target is a ClassDecl) and emit-phase IR hint.

decorator sealed(target) {
typecheck {
// Custom rule: sealed classes must not have subclass-ready methods
require !target.is_abstract, "sealed class cannot also be abstract";
}
emit {
// Backend hint: mark class as sealed for LIR/layout passes
directive("sealed", target);
}
runtime {
// Runtime guard: prevent extension via extend syntax check
return call.proceed();
}
}

decorator audited(target) {
compile { emit(`target.__audited = true;`); } // stamp metadata
runtime { print("audited creation of " + target.name); return call.proceed(); }
}

// Order matters: @sealed outer, @audited inner
@sealed
@audited
class Account {
balance: f64,
owner: string,
}

let a = Account { balance: 100.0, owner: "Adesh" };
print(a.__audited); // true — injected by audited's compile phase
// If order were @audited @sealed, the wrappers would nest oppositely:
// @audited outer → audited.runtime wraps sealed.runtime wraps Account construction
// Pipeline hash captures this: swapping order changes DecoratorPipeline.pipeline_hash

// Decorator on a struct field family via field decorator (if supported):
// @readonly let x: i32 = 1; // see readonly keyword — enforced by type system

Restrictions / Errors

KindTriggerDiagnosticHelp
Lexicallet decorator = 1unexpected token 'decorator', expected identifierTokenKind::Decorator reserved (lexer.rs:1004)
Parsedecorator without nameExpect identifier after 'decorator'decorator <name>(...) { phases }
Parsedecorator name { } with no phaseNo error — valid empty decorator (no-op)Add at least one phase if transform is intended
Parse@ with no identExpect decorator name after '@'Write @logged or @retry(3)
ParsePhase outside decorator compile {} at top levelNot a decorator — parsed as standalone compile blockWrap multi-phase logic inside decorator name(...) { ... }
Semanticcall.proceed() outside runtimeproceed is only valid inside runtime phase (TokenKind::Proceed)Move delegation to runtime {}
Semanticemit outside compile/emitemit is only valid inside compile or emit phaseEmit only inside those two phases
Semantic@unknown fn f() unknown decoratorunknown decorator 'unknown'Define decorator unknown(...) { ... } before use
SemanticArity mismatch @retry() with no argsmissing required argument 'n' for decorator 'retry'Provide @retry(3) matching decorator retry(n: i32)
PipelineUnsafe decorator on safe sitedecorator requires unsafe but site is not unsafe (DecoratorDef.requires_unsafe)Add unsafe to site or remove unsafe from decorator
TypeDecorator applied to unsupported decldecorator 'x' cannot be applied to <kind>Check DecoratorDef applicability (some restrict to fn/class)
HashReordered @a @b vs @b @aNo error — different pipeline_hashIntended: order defines wrapping; be deliberate

Common pitfall — forgetting that @a @b is outer→inner:

decorator a(target) { runtime { print("a before"); let r = call.proceed(); print("a after"); return r; } }
decorator b(target) { runtime { print("b before"); let r = call.proceed(); print("b after"); return r; } }

@a @b fn f() { print("f"); }

// Execution order on f(): a before → b before → f → b after → a after
// NOT b before → a before. Outermost annotation is outermost wrapper.

@b @a fn g() { print("g"); } // g(): b before → a before → g → a after → b after

Use outermost position for the decorator that should see the most-abstract view (e.g., logging outside, caching inside).


See Also

  • compileTokenKind::Compile / DecoratorPhase::Compile
  • runtimeTokenKind::Runtime / call.proceed() (TokenKind::Proceed)
  • typecheckTokenKind::Typecheck phase for decorator type constraints
  • emitTokenKind::Emit backend phase and string emission
  • requireTokenKind::Require contracts inside any phase
  • readonlyTokenKind::Readonly field modifier (often set via compile emit)
  • jumpTokenKind::Jump low-level control flow (not a decorator phase)
  • Decorators — high-level guide and additional patterns
  • src/parsing/lexer.rs:1004,1016-1021, src/parsing/ast.rs:687-737, src/parsing/decorator_registry.rs, src/parsing/decorator_pipeline.rs, src/parsing/decorator_compile.rs