Thursday, February 10, 2011

Cecil.Decompiler and F#

In this new post-Reflector age, I thought I'd have a look-see how the main competition worked on F#. So I whipped up a simple driver for Cecil.Decompiler:

private static void Decompile(MethodDefinition m)
{
try {
Formatter.WriteMethodBody(Console.Out, m);
var lang = CSharp.GetLanguage(CSharpVersion.V3);
var writer = lang.GetWriter(new PlainTextFormatter(Console.Out));
writer.Write(m);
} catch (Exception e) {
Console.WriteLine(m.FullName);
Console.WriteLine(e.Message);
}
}
private static void DumpAssembly(string path)
{
var assembly = AssemblyDefinition.ReadAssembly(path);
var pdbpath = Path.ChangeExtension (path, ".pdb");
var provider = new PdbReaderProvider();
var reader = provider.GetSymbolReader(assembly.MainModule, pdbpath);
assembly.MainModule.ReadSymbols(reader);
assembly.MainModule.Types.SelectMany(x => x.Methods).ToList().ForEach(Decompile);
}
view raw gistfile1.cs hosted with ❤ by GitHub

and tried it on an assembly of some fairly simple F# -- a few extension methods for Option

module Augment =
type Microsoft.FSharp.Core.Option<'T> with
static member filter (f : 'T -> bool) (x : option<'T>) =
match x with
| Some v when f(v) -> Some v
| _ -> None
static member getOrElse (fallback : 'T) (x : option<'T>) = defaultArg x fallback
static member select (f : 'T -> bool) (x : 'T) =
if f(x) then Some x
else None
static member nullable (x : 'a when 'a : null) : option<'a> =
if x <> null then Some x
else None
view raw gistfile1.fs hosted with ❤ by GitHub

and we get things like

public static FSharpOption<T> Option`1.select.Static(FSharpFunc<T, bool> f, T x)
{
if (f.Invoke(x))
{
}
return FSharpOption<T>.Some(x);
return null;
}
view raw gistfile1.cs hosted with ❤ by GitHub

This is better than Reflector was when I first tried it about 18 months ago -- F#'s cluster of branch instructions

IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: callvirt !1 Microsoft.FSharp.Core.FSharpFunc`2<T,System.Boolean>::Invoke(!0)
IL_0008: brfalse.s IL_000c
IL_000a: br.s IL_000e
IL_000c: br.s IL_0015
IL_000e: ldarg.1
IL_000f: call Microsoft.FSharp.Core.FSharpOption`1<!0> Microsoft.FSharp.Core.FSharpOption`1<T>::Some(!0)
IL_0014: ret
IL_0015: ldnull
IL_0016: ret
view raw gistfile1.cs hosted with ❤ by GitHub

caused the old Reflector to just crash.

F#'s habit of lacing in temporaries did flummox the decompilation a bit:

public static FSharpOption<T> Option`1.filter.Static(FSharpFunc<T, bool> f, FSharpOption<T> x)
{
if (x)
{
FSharpOption<T> = ;
T v = .get_Value();
if (f.Invoke(v))
{
}
T v = .get_Value();
return FSharpOption<T>.Some(v);
}
return null;
}
view raw gistfile1.cs hosted with ❤ by GitHub

compared with the current Reflector's take of

public static FSharpOption<T> Option`1.filter.Static<T>(FSharpFunc<T, bool> f, FSharpOption<T> x)
{
FSharpOption<T> option = x;
if (option != null)
{
FSharpOption<T> option2 = option;
T v = option2.get_Value();
if (f.Invoke(v))
{
return FSharpOption<T>.Some(option2.get_Value());
}
}
return null;
}
view raw gistfile1.cs hosted with ❤ by GitHub

MonoDevelop 2.4.2.'s Assembly Browser gets it a bit better than the raw decompiler, though it still has the same 'C'-style if in there:

public static FSharpOption<T> Option`1.filter.Static(FSharpFunc<T, bool> f, FSharpOption<T> x)
{
if (x)
{
FSharpOption<T> V_1 = V_0;
T V_2 = V_1.get_Value();
if (f.Invoke(V_2))
{
}
T V_3 = V_1.get_Value();
return FSharpOption<T>.Some(V_3);
}
return null;
}
view raw gistfile1.cs hosted with ❤ by GitHub

Amusingly, I was able to get the decompilation to throw by feeding the little driver program into itself -- the DumpAssembly method would not turn back into C#! MonoDevelop silently refused to load the contents of the whole assembly.

No comments :