LEVIATHAN v962456e · 962456eee1

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. replace needs a rewrites or generates header, and inject is not allowed in a rule that has one. Either mistake is a compile error.
  • $body splices 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 in var result = $body;, it is valid only when the original body is a single value: an arrow body => e or a block that is exactly { return e; }. Any other body is a compile error, and the way around it is to use $body in statement position.
  • $body must 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, use lang.generates.
  • The matched bind must be a callable (a method or function).
  • Ordering: all additive injections into a body apply first, and the one replace then wraps the result. So $body stands 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, rewrites or generates, 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 N changes the budget. Only reentrant rules 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

  • rewrites and generates can each be combined with reentrant on one rule, in either order.
  • The rewrite shows up in --expand as the new body, so reading the expanded program shows what ran.

See also