The self-hosting `derive` macro

I came up with a derive macro that specializes match as a refactoring tool for self-hosted programs. I built it on top of blossoms

I think it's fun to use because any position in expression context can reflect on its role in another expression, using all values available at runtime. Source code to be published in new Denxi edition I hope to have out by the end of the month.

For now, I'll just go over the current syntax and behavior of the macro.

(module+ test
  (derive derivative
          #:as (declassify derivative)
          #:taxa rule-position
          #:diff
          [`(derive derivative ,_ ... #:diff [,datum ,_] ,_ ...)
           (rule-position datum)]
          [(list-no-order (? rule-position? rp) _ ...)
           rp]))

A derive call expresses all the s-exp level edits you'd like to make to the surrounding program if your text editor's cursor sat at the corresponding call site. This specific example is kind of neat because the first match pattern under #:diff is targeting itself. The result of the expression is the datum form of that pattern.

(quote (quasiquote (derive derivative
                           (unquote _) ...
                           #:diff
                           [(unquote datum) (unquote _)]
                           (unquote _) ...)))

In general, (derive ...) is a copy of the entire surrounding source code as a list, refactored using a front-end to match. The match clauses under #:diff control what code is replaced with new values. The clauses match elements provided in child-to-parent order, such that the (derive ...) expression itself is the first expression used in match. Next, it's the (module+ test ...), then any hypothetical traversal of proper or improper lists leading to the topmost enclosing S-expression. If no clauses match for an element at some level, then that element stays. All uses of match occur during runtime. The derive macro only serves to associate the call site with the runtime behavior.

Every element of an (im)proper list has its own child-to-parent traversal, so it is possible to distinguish equal? values (like copies of module forms) based on these traversals. This is a nice property, because it means that any lexically-equal derive starts operating exactly on itself. This bleeds over into other benefits, like module forms containing programs that can recognize their position in the whole module tree.

Finally, the derivative, #:as (declassify derivative), and #:taxa are all related. The first derivative identifier binds the result of the final use of match. The expression after #:as uses that binding to compute the value of the whole derive macro, kind of like how #:result works in for/fold. The declassify function is an accessor that returns the value encapsulated by the ad-hoc #:taxa (we don't want a rule-position classifier, we want the classified value). The #:taxa identifiers help generate temporary constructors and predicates that classify values without ambiguity in match patterns.

This example works by having a match pattern extract the datum form of itself to classify accordingly. Since I'm traversing towards parent values, the list-no-order rule naturally propagates classified elements. #:as returns the element itself, without the classifying wrapper. In effect, this replaces the whole program's source code with just that one match pattern.

3 Likes