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);    
  }

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

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;
}

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

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;
}

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;
}

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;
}

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.

Post a Comment