Metaprogramming
Body-replacing rules — rewrites, replace and $body
A rewrites body of rule replaces a method's body with a template that splices the original back in with $body.
since 0.1.0-alpha.1linuxwindowswasm
Description
An ordinary rule only adds code. A body-replacing rule is the one capability that changes a body that
you wrote, and it needs an explicit header so that it is easy to find and its --expand difference is
obvious. The header is rewrites body of <bind>, where <bind> is the matched method or function. The rule
supplies a replace template instead of inject … at …:
rule name rewrites body of m {
match @Attr on method m
replace `<template>`
}
The template is a statement fragment. $body splices the original body into it, so a rewrite wraps the old
body instead of discarding it. This rule counts calls around any method marked @Timed:
Wrapping a method with $body
namespace Perf {
attribute Timed { }
rule timed rewrites body of m {
match @Timed on method m
replace `
calls = calls + 1;
var result = $body;
return result;
`
}
}
uses Perf;
class Service {
int calls = 0;
@Timed
int square(int n) => n * n;
}
Service s = Service();
console.writeln(s.square(3));
console.writeln(s.square(4));
console.writeln(s.calls);
9
16
2
$body in var result = $body; stands for the value of the original arrow body n * n.
Rules
- A rule is additive (
inject) or a rewriter (replace), never both.replaceneeds arewritesorgeneratesheader, andinjectis not allowed in a rule that has one. Either mistake is a compile error. $bodysplices the original body back in. In statement position, as in$body;, it splices the original statements as they were written; their local variables are not renamed, because the body moves as one unit. In expression position, as invar result = $body;, it is valid only when the original body is a single value: an arrow body=> eor a block that is exactly{ return e; }. Any other body is a compile error, and the way around it is to use$bodyin statement position.$bodymust appear exactly once. A template that never mentions it would silently drop the original body, and one that mentions it twice would duplicate its side effects; both are compile errors. To drop the body on purpose, uselang.generates.- The matched bind must be a callable (a method or function).
- Ordering: all additive injections into a body apply first, and the one
replacethen wraps the result. So$bodystands for the method as written plus any prologue and epilogue the other rules added. - Two body replacements on one body do not compose. When two rules,
rewritesorgenerates, replace the same body, the compiler reports a conflict that names both rules. - By default a rule never matches code that another rule injected. A rule declared
reentrant(rule name reentrant { … }) opts in: it can match injected code and is re-run until nothing new is produced. If it does not settle within the round budget, which is 8 by default, the compiler reports an error;--reentrant-budget Nchanges the budget. Onlyreentrantrules see rule-generated code, so the rest see the program exactly once.
Examples
$body in statement position runs the original statements after the new prologue:
$body as a statement
namespace Guard {
attribute Logged { }
rule logged rewrites body of m {
match @Logged on method m
replace `
log = log + "enter;";
$body;
`
}
}
uses Guard;
class Tracker {
string log = "";
@Logged
void touch() {
log = log + "work;";
}
}
Tracker t = Tracker();
t.touch();
console.writeln(t.log);
enter;work;
An additive rule's injection comes first, and the rewrite wraps the result:
inject first, replace second
namespace Order {
attribute Wrapped { }
rule prologue {
match @Wrapped on method m
inject `log = log + "prologue;";` at top of body
}
rule wrap rewrites body of m {
match @Wrapped on method m
replace `
log = log + "[";
$body;
log = log + "]";
`
}
}
uses Order;
class Job {
string log = "";
@Wrapped
void run() {
log = log + "body;";
}
}
Job j = Job();
j.run();
console.writeln(j.log);
[prologue;body;]
A reentrant rule matches code another rule injected. seed adds a method that carries @Grown, and grow
matches that method, which an ordinary rule would not see:
A reentrant rule
namespace Gen {
attribute Seed { }
attribute Grown { }
rule seed {
match @Seed on class C
inject `@Grown int grown() => 10;` at member of C
}
rule grow reentrant {
match @Grown on method m in class C
inject `int doubled() => 20;` at member of C
}
}
uses Gen;
@Seed
class Widget { }
Widget w = Widget();
console.writeln(w.grown());
console.writeln(w.doubled());
10
20
Notes
rewritesandgeneratescan each be combined withreentranton one rule, in either order.- The rewrite shows up in
--expandas the new body, so reading the expanded program shows what ran.
See also
- Rules — match a shape, inject code — A rule matches declarations by shape and injects quasiquoted code at a named anchor, with
match,where, andinject … at. - Body-generating rules — generates and replace — A
generates body ofrule replaces a method's body outright and discards the original, for stubs whose body is machine-filled. - Quasiquote templates and holes — Backtick-delimited templates hold the code a rule injects, and
$holes fill them from the match. - Compile-time metaprogramming — A map of the four compile-time layers, procedural macros, and the
--expandflag that shows what a rule produced.