Defining functions at top level without using macros

Maybe I should explain in a more concrete way what I’d like to do and why I’d like to do it. I’m working on a mutation testing library called Darwin (forum thread here: Mutation testing - Mutating BEAM bytecode). Despite the title, I’m not mutating the BEAM bytecode. Instead, I will extract the erlang abstract code (that’s the same as the Erlang AST) from the .beam files and mutate that.

To avoid recompiling the modules to many times, I have a way of adding hundreds of mutations to an Erlang module in such a way that I can toggle the mutations on or off. That way, by flipping a switch (probably an entry in an ETS table, or something like that), I can decide which mutation I want to activate without recompiling the code. I expect that will save a lit of time over something like exavier, which requires either (1) recompiling the code for each mutation or (2) insert lots of mutations at the same time, thus losing fine grianed control on what we’re testing.

As I said, my approach requires replacing instances of

A + B.
% in abstract code:
{:op, 1, :+, {:var, 1, :A}, {:var, 1, :B}}

and replacing that with something like

'Elixir.Darwing.Mutator':do_op_add_mutator(A, B).
% in abstract code:
{:call, 1,
 {:remote, 1, {:atom, 1, Darwin.Mutator}, {:atom, 1, :do_op_add_mutator}},
 [{:var, 1, :A}, {:var, 1, :B}]}

Where the 'Elixir.Darwing.Mutator':do_op_add_mutator/2 function contains the “machinery” to either execute the original code (A + B.) or one of the possible mutations (e.g. A - B., A * B, A, B, etc.). Writing the function in full is a little tedious and error prone, and it must be repeated for a bunch of operators (the arithmetic operators, comparison operators, etc). I’ve decided to simplify things by writing some suitable macros. Currently, I have this for the 4 arithmetic operators:

defmodule Darwin.Mutators do
  # don't worry about the macro in this module; it's a pretty simple one
  import Darwin.Mutator.Helpers.DefHelper, only: [def_bin_op_mutator: 4]

  def_bin_op_mutator(:op_add_mutator, :+, &Kernel.+/2, [
    [f: fn a, _b -> a end, text: "delete right operand from '+' (e.g. 'a + b' -> 'a')"],
    [f: fn _a, b -> b end, text: "delete left operand from '+' (e.g. 'a + b' -> 'b')"],
    [f: fn a, b -> a - b end, text: "replace operator '+' by '-'"],
    [f: fn a, b -> a * b end, text: "replace operator '+' by '*'"],
    [f: fn a, b -> a / b end, text: "replace operator '+' by '/'"],
    [f: fn a, b -> rem(a, b) end, text: "replace operator '+' by 'rem'"]
  ])

  def_bin_op_mutator(:op_sub_mutator, :-, &Kernel.-/2, [
    [f: fn a, _b -> a end, text: "delete right operand from '-' (e.g. 'a - b' -> 'a')"],
    [f: fn _a, b -> b end, text: "delete left operand from '-' (e.g. 'a - b' -> 'b')"],
    [f: fn a, b -> b - a end, text: "switch order of arguments to '-' (e.g. 'a - b' -> 'b - a')"],
    [f: fn a, b -> a + b end, text: "replace operator '-' by '+'"],
    [f: fn a, b -> a * b end, text: "replace operator '-' by '*'"],
    [f: fn a, b -> a / b end, text: "replace operator '-' by '/'"],
    [f: fn a, b -> rem(a, b) end, text: "replace operator '-' by 'rem'"]
  ])

  def_bin_op_mutator(:op_mul_mutator, :*, &Kernel.+/2, [
    [f: fn a, _b -> a end, text: "delete right operand from '*' (e.g. 'a * b' -> 'a')"],
    [f: fn _a, b -> b end, text: "delete left operand from '*' (e.g. 'a * b' -> 'b')"],
    [f: fn a, b -> a + b end, text: "replace operator '*' by '+'"],
    [f: fn a, b -> a - b end, text: "replace operator '*' by '-'"],
    [f: fn a, b -> a / b end, text: "replace operator '*' by '/'"],
    [f: fn a, b -> rem(a, b) end, text: "replace operator '*' by 'rem'"]
  ])

  def_bin_op_mutator(:op_div_mutator, :/, &Kernel.//2, [
    [f: fn a, _b -> a end, text: "delete right operand from '/' (e.g. 'a / b' -> 'a')"],
    [f: fn _a, b -> b end, text: "delete left operand from '/' (e.g. 'a / b' -> 'b')"],
    [f: fn a, b -> b / a end, text: "switch order of arguments to '/' (e.g. 'a / b' -> 'b / a')"],
    [f: fn a, b -> a + b end, text: "replace operator '/' by '+'"],
    [f: fn a, b -> a - b end, text: "replace operator '/' by '-'"],
    [f: fn a, b -> a * b end, text: "replace operator '/' by '*'"],
    [f: fn a, b -> rem(a, b) end, text: "replace operator '/' by 'rem'"]
  ])
end

The code above defines four functions, one for each arithmetic operator. I was forced to add these large keyword lists inside the calls to the def_bin_op_mutator/4 macro (and use keyword lists instead of something else) so that the macros can receive some nice AST to work with.

But if you notice, the lists have too many repetition, and could be generated programmatically from a much smaller list, marking it easier to refactor and leaving less room for typos. That’s why I’d like to be able to manipulate the keyword lists before feeding them into the macros…

Just to be clear: I’m not asking for help implementing the Erlang abstract code mutations. The question is the one in the post above, this is just to provide some additional context.

There are two legitimate questions at this point:

  1. Am going too deep into macro wizardry just to avoid writing a couple of function bodies? Maybe, but the truth is that I trust my macros to generate said function bodies much more than I trust myself to write them manually

  2. Since I’m mutating Erlang abstract code, nothing here is elixir-specific. Should the whole library should be written in Erlang to start with, so that it can be used with Erlang projects? The answer to this is probably yes, but Elixir is a much nicer language for a prototype (since I can use macros to hide literal screens full of boring case statements and copy-pasted code). I’ll think about rewriting the whole thing in Erlang if it ever becomes stable.

1 Like