Arraigning a Statement, vol. 1
This time in our “Mutating Rust” series, we want to tackle the most complex mutation so far:
Statement removal. Now why do I think this is complex? It’s just removing the statement (or, as we
bake our mutations into the code, activating at runtime, putting it behind an if
), right?
Let’s take a very simple example:
a();
b();
We want to be able to omit running the a()
or the b()
, depending on which mutation is active.
And in this simple example, it would easily work. Our mutated code might look like:
if !::mutagen::now(22) { a(); }
if !::mutagen::now(23) { b(); }
However, not all Rust code is that simple. The first obvious complication is that statements may
include let
bindings, for example:
let x = 1;
println!("{}", x);
Trying the same simple logic would lead to the following code:
if !::mutagen::now(22) { let x = 1; }
if !::mutagen::now(23) { println!("{}", x); }
This leads to a compiler error, because the let
binding is no longer in scope of the
println!(..)
statement. Ok, so we need to keep track of locals. Since Rust allows, nay,
encourages shadowing, we have to keep all let
statements, because they could change types, e.g.
the following compiles (though it will warn because the first x
is unused):
let x = "Hi!";
let x = "Ho!";
println!("{}", x);
Ok, but let bindings only work within the same scope, so we at least don’t need to care for nested expressions? Unfortunately for us, Rust allows deferred initialization, as in the following example:
let x;
{ x = 1; } // Voilà, nested block!
println!("{}", x);
Well, at least we know all statements always only apply to code after the original statement? There is one counter-example here, too:
println!("{}", X);
const X : &str = "Hi!";
(Update: The original version stated that static
obeyed ordering, but this was tested against some
Rust 1.2x and it appears that resolve has changed in the meantime. Bummer)
So we have to filter static
and const
item statements from removal, but that’s about it.
Luckily for us, both bindings for const
and static
obey scope boundaries, so we need not fear
nested statements in this vein:
// println!("{}", X); // ← Error: X not in scope
{
const X: &str = "Ouch";
}
So for a minimum viable product we can remove free function-call statements, unless the arguments contain any assignment or the return value is the return value of the block. Not much, but a start. Stay tuned for the next entry in this series!