Friday, May 13, 2022

F# under the covers XIX -- `with` clauses

Once again, the half-year update has included some interesting changes in code generation, this time around try/with clauses.

Two cases to show this, one simple, one more complex. As always, decompilations are for debug (unoptimized) builds, though in this case, the release builds differ only in details of the branch instructions.

      try
        ...
      with
      | :? FormatException -> Default
      try
        ...
      with
      | :? ArgumentException as a -> a |> (logException store)
      | :? NotSupportedException as n -> n |> (logException store)
      | :? IOException as i -> i |> (logException store)
      | :? System.Security.SecurityException as s -> s |> (logException store)
      | :? UnauthorizedAccessException as u -> u |> (logException store)

Before the 6.0.300 SDK update, the simple case was entirely direct in its handling of the exception

	.try
	{
	...
		IL_006d: leave.s IL_0098
	} // end .try
	catch [netstandard]System.Object
	{
		IL_006f: castclass [netstandard]System.Exception
		IL_0074: stloc.s 8
		IL_0076: ldloc.s 8
		IL_0078: isinst [netstandard]System.FormatException

but afterwards it becomes much fancier, with duplication of effort in the handled case, and only makes sense if the exit from the filter in the unhandled case is faster than from a catch.

	.try
	{
        ...
		IL_006e: leave.s IL_00b5
	} // end .try
	filter
	{
		IL_0070: castclass [netstandard]System.Exception
		IL_0075: stloc.s 8
		IL_0077: ldloc.s 8
		IL_0079: isinst [netstandard]System.FormatException
		IL_007e: stloc.s 9
                ...
	} // end filter
	catch
	{
		IL_008c: castclass [netstandard]System.Exception
		IL_0091: stloc.s 10
		IL_0093: ldloc.s 10
		IL_0095: isinst [netstandard]System.FormatException
		IL_009a: stloc.s 11
                ...
	} // end handler

the new code being equivalent to C# catch (object obj2) when ((((Exception)obj2) is FormatException) ? true : false)

The more complex case was like

	catch [netstandard]System.Object
	{
		IL_0010: castclass [netstandard]System.Exception
		IL_0015: stloc.1
		// ArgumentException ex2 = ex as ArgumentException;
		IL_0016: ldloc.1
		IL_0017: isinst [netstandard]System.ArgumentException
		IL_001c: stloc.2
		// if (ex2 == null)
		IL_001d: ldloc.2
		IL_001e: brfalse.s IL_0022
        ...

or, decompiled

	catch (object obj)
	{
		Exception ex = (Exception)obj;
		ArgumentException ex2 = ex as ArgumentException;
		if (ex2 == null)
		{
			NotSupportedException ex3 = ex as NotSupportedException;
			if (ex3 == null)
			{
			// ... throw on exhaustion
			}
			NotSupportedException j = ex3;
			Exception e4 = j;
			logException(store, e4);
			return result;
		}
		ArgumentException a = ex2;
		Exception e5 = a;
		logException(store, e5);
		return result;
	}

but now it looks like

	catch [netstandard]System.Object
	{
		IL_0010: castclass [netstandard]System.Exception
		IL_0015: stloc.1
		// object obj2 = ex;
		IL_0016: ldloc.1
		IL_0017: stloc.2
		// if (!(obj2 is ArgumentException))
		IL_0018: ldloc.2
		IL_0019: isinst [netstandard]System.ArgumentException
		IL_001e: ldnull // omitted in release
		IL_001f: cgt.un // omitted in release
		IL_0021: brtrue.s IL_0065
...
		IL_0065: ldloc.1
		IL_0066: unbox.any [netstandard]System.ArgumentException
		IL_006b: stloc.s 7

or in C#

	catch (object obj)
	{
		Exception ex = (Exception)obj;
		object obj2 = ex;
		if (!(obj2 is ArgumentException))
		{
			object obj3 = ex;
			if (!(obj3 is NotSupportedException))
			{
			// ... throw on exhaustion
			}
			NotSupportedException j = (NotSupportedException)(object)ex;
			NotSupportedException ex5 = j;
			bool store5 = store;
			NotSupportedException e4 = ex5;
			logException(store5, e4);
			return result;
		}
		ArgumentException a = (ArgumentException)(object)ex;
		ArgumentException ex6 = a;
		bool store6 = store;
		ArgumentException e5 = ex6;
		logException(store6, e5);
		return result;
	}

which discards the results of the isinst instructions, then does an unbox.any for any case that actually matches; which could provide a marginal space improvement, in the case where no clause matches -- i.e. another possible optimization the unhandled exception scenario; which in both cases are the "I don't care, this has just failed completely" route.