[nem-en] Operator ??

Ivan A Eryshov ivan.eryshov at gmail.com
Sun Sep 24 20:08:08 CEST 2006


Michal,

I inspect C# specification regarding nullable types and the null coalescing
operator ('??'). '??' has a lot of subtleties and I trying consistent with
them. Here is a piece of C# specification:

[

*14.12 The null coalescing operator
*The ?? operator is called the null coalescing operator.
*null-coalescing-expression:
conditional-or-expression
conditional-or-expression ?? null-coalescing-expression
*A null coalescing expression of the form a ?? b requires a to be of a
nullable type or reference type. If a is
non-null, the result of a ?? b is a; otherwise, the result is b. The
operation evaluates b only if a is null.
The null coalescing operator is right-associative, meaning that operations
are grouped from right to left.
[Example: An expression of the form a ?? b ?? c is evaluated as a ?? (b ??
c). In general terms, an
expression of the form E1 ?? E2 ?? ... ?? EN returns the first of the
operands that is non-null, or null if all
operands are null. end example]
The type of the expression a ?? b depends on which implicit conversions are
available between the types
of the operands. In order of preference, the type of a ?? b is A0, A, or B,
where A is the type of a, B is the
type of b, and A0 is the type that results from removing the trailing ?
modifier, if any, from A. Specifically,
a ?? b is processed as follows:
• If A is not a nullable type or a reference type, a compile-time error
occurs.
• If A is a nullable type and an implicit conversion exists from b to A0,
the result type is A0. At run-time, a
is first evaluated. If a is not null, a is unwrapped to type A0, and this
becomes the result. Otherwise, b is
evaluated and converted to type A0, and this becomes the result.
• Otherwise, if an implicit conversion exists from b to A, the result type
is A. At run-time, a is first
evaluated. If a is not null, a becomes the result. Otherwise, b is evaluated
and converted to type A, and
this becomes the result.
• Otherwise, if an implicit conversion exists from A0 to B, the result type
is B. At run-time, a is first
evaluated. If a is not null, a is unwrapped to type A0 (unless A and A0 are
the same type) and converted
to type B, and this becomes the result. Otherwise, b is evaluated and
becomes the result.
Otherwise, a and b are incompatible, and a compile-time error occurs.

]

So, I want to implement all these rules. I wrote macro:


 using System.Console;

using Nemerle;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Text;

namespace Macro
{
  /** This module is used to simplify work with op_Implicit operator.

      This module is stateless.
   */
  module OpImplicitHelper
  {
    /**
     * Used to print all implicit operators of some type
     */
    public PrintAll(cl : MType.Class) : void
    {
      def members = cl.tycon.LookupMember("op_Implicit");
      foreach (m is IMethod in members)
        WriteLine(m);
    }

    /**
     * Used to determine has a `from' type implicit conversation
     * to a `to' type or hasn't.
     */
    public HasImplicit(from : MType.Class, to : MType.Class) : bool
    {
      def members = to.tycon.LookupMember("op_Implicit")
                    + from.tycon.LookupMember("op_Implicit");
      ret :
      {
        foreach (m is IMethod in members)
        {
          def tyRet = m.ReturnType;
          def tyPar = m.GetParameters().Head.ty;
          when (tyRet.Equals(to) && tyPar.Equals(from))
            ret (true);
        }
        false;
      }
    }
  }

  macro @printImplicit(expr)
  {
    def typer = Macros.ImplicitCTX ();
    def tx = typer.TypeExpr (expr);
    match (tx.Type.Hint)
    {
      | Some (Class (_ti, _args) as cl) =>
          OpImplicitHelper.PrintAll(cl);
          <[]>
      | _ =>
          Message.FatalError ("PrintImplicit macro can not be used with such
expression.");
    }
  }

  macro @test(expr)
  {
    <[ if ($expr != null) $expr.Value; else (CI () : int) ]>
  }

  macro @?? (exprA, exprB)
  {
    def typer = Macros.ImplicitCTX ();
    def tx = (typer.TypeExpr(exprA).Type.Hint, typer.TypeExpr
(exprB).Type.Hint);
    def teA = typer.TypeExpr(exprA);
    def teB = typer.TypeExpr(exprB);
    def tyA = typer.TypeExpr(exprA).Type;
    def tyB = typer.TypeExpr(exprB).Type;

    def upCast =
      (texpr, ty) =>
        TExpr.TypeConversion (texpr.loc, ty, texpr, ty,
                              ConversionKind.UpCast());
    def _implicit =
      (texpr, ty) =>
        TExpr.TypeConversion (texpr.loc, ty, texpr, ty,
                              ConversionKind.Implicit());

    match (tx)
    {
      | (Some(Class(tiA, [tyA0]) as clA), Some(Class(_, _) as clB))
        when tiA.IsValueType && tiA.FullName == "System.Nullable" =>

          def clA0 = tyA0 :> MType.Class;
          if (clA0.Equals(clB)
              || OpImplicitHelper.HasImplicit(clB, clA0))
          {
            /**
             *  BUG: 'Nullable[T]' can not be converted to 'T',
             *       but it should.
             *  Example:
             *    def inull : int? = 10
             *    // cannot convert System.Nullable[int] to int:
             *    def i : int = inull :> int
            **/
            def st = <[
              if ($exprA != null)
                $exprA.Value
              else
                $(upCast(teB, tyA0) : typed)
            ]>;
            WriteLine($"nullable: B -> A0: $st");
            st
          }
          else if (clA.Equals(clB)
                   || OpImplicitHelper.HasImplicit(clB, clA))
          {
            /**
             *  BUG: 'Nullable[T1]' can not be converted to
             *       'Nullable[T2]' when 'T1' has implicit
             *       conversation to 'T2', but it should.
             *  C# Example:
             *    int? i = 10;
             *    double? d = i;
            **/
            def st = <[
              if ($exprA != null)
                $exprA
              else
                $(upCast(teB, tyA) : typed)
            ]>;
            WriteLine($"nullable: B -> A: $st");
            st
          }
          else if (OpImplicitHelper.HasImplicit(clA0, clB))
          {
            /**
             *  BUG: 'T1' can not be converted to 'Nullable[T2]'
             *       when 'T1' has implicit conversation to 'T2',
             *       but it should.
             *  C# Example:
             *    int i = 10;
             *    double? d = 0.1;
             *    d = i;
            **/
            def st = <[
              if ($exprA != null)
                $(upCast(typer.TypeExpr(<[ $exprA.Value ]>), tyB) : typed)
              else
                $exprB
            ]>;
            WriteLine($"nullable: A0 -> B: $st");
            st
          }
          else
          {
            Message.FatalError ($"Operator `??' cannot be applied to "
                                "operands of type `$clA' and `$clB'");
          }

      | (Some(Class(tiA, _)), Some(Class(_, _)))
        when tiA.IsValueType =>

          Message.FatalError (exprA.Location,
                              $"`$tiA' is not a reference or nullable type "
                              "as required by the `??' operator");

      | (Some(Class(_, _) as clA), Some(Class(_, _) as clB))
        when clA.Equals(clB) || OpImplicitHelper.HasImplicit(clB, clA) =>

          /**
           * ?BUG(??): This match case and next one can be replaced with
single
           *       one, but followed by code doesn't compile. Is this a
design
           *       decision or a bug? (C# compile such code with no
problems)
           *  Example:
           *    #pragma indent
           *    class A {}
           *    class B
           *      public static @:(_ : B) : A
           *        A()
           *    def a = A()
           *    def b = B()
           *    // expected B-, got A in computation branch:
           *    def c = if (a==null) b else a
          **/
          def st = <[
            if ($exprA != null)
              $exprA
            else
              $(upCast(teB, tyA) : typed)
          ]>;
          WriteLine($"reference: B -> A: $st");
          st

      | (Some(Class(_, _) as clA), Some(Class(tiB, _) as clB))
        when OpImplicitHelper.HasImplicit(clA, clB) =>

          def st = <[
            if ($exprA != null)
              $(upCast(teA, tyB) : typed)
            else
              $exprB
          ]>;
          WriteLine($"reference: A -> B: $st");
          st

      | _ =>
          Message.FatalError ($"Operator `??' cannot be applied to "
                              "operands of type `$tyA' and `$tyB'");
    }
  }
}

But it's works badly.

Firstly, you can see several comments in macro code. This
comments describe a bunch of bugs with nullable types. Also, one comment
marked with (??), I not sure is it a bug or function as designed.
Secondly, I have problems with ?? macro (may be this is some kind of bugs,
not sure). Let's look on macro usage:


#pragma indent

using System.Console;
using Macro;


class C1
  public override ToString() : string
    "C1"
  public Test() : void
    WriteLine("C1::Test" );

class C2
  public static @:(_ : C2) : C1
    C1()
  public override ToString() : string
    "C2"

class CI
  public static @:(_ : CI) : int
    2

def getType['t] (_ : 't)
  $ "$(typeof('t))"


def c1 = null : C1
def c2 = C2()
def r1 = c1 ?? c2 // *(***)*compile-time message: "reference: B -> A: if (c1
!= null) c1; else (c2 : C1)"
def c1 = C1()
def t1 = getType(r1)
WriteLine($ "TEST REFERENCE B -> A (result):  $r1" )
WriteLine($"TEST REFERENCE B -> A (type):  $t1")
// run-time messages:
// TEST REFERENCE B -> A (result):  *C2*
// TEST REFERENCE B -> A (type):  C1

def r2 = c2 ?? c1 // compile-time message: "reference: A -> B: if (c2 !=
null) (c2 : C1); else c1"
def t2 = getType(r2)
WriteLine($"TEST REFERENCE A -> B (result):  $r2")
WriteLine($"TEST REFERENCE A -> B (type):  $t2" )
// run-time messages:
// TEST REFERENCE B -> A (result):  *C2*
// TEST REFERENCE B -> A (type):  C1

def r0 = if (c1 == null) (c2 : C1) else c1 // *the same code expected as
macro result in (***) *
def t0 = getType(r0)
WriteLine($"TEST REFERENCE B -> A (result):  $r0")
WriteLine($"TEST REFERENCE B -> A (type):  $t0" )
// run-time messages: *this result I expect in 2 previous cases
*// TEST REFERENCE B -> A (result):  *C1*
// TEST REFERENCE B -> A (type):  C1



def inull_v : int? = 10
def inull_n : int? = null
def r3 = inull_v ?? 1 // compile-time message: "nullable: B -> A0: if
(inull_v != null) inull_v.Value; else (1 : int)"
def t3 = getType(r3)
WriteLine($"TEST NULLABLE B -> A0 (result):  $r3" )
WriteLine($"TEST NULLABLE B -> A0 (type):  $t3" )
// run-time messages: OK, this is what I expected.
// TEST NULLABLE B -> A0 (result):  10
// TEST NULLABLE B -> A0 (type):  System.Int32

def r4 = inull_n ?? 1 // compile-time message: "nullable: B -> A0: if
(inull_n != null) inull_v.Value; else (1 : int)"
def t4 = getType(r4)
WriteLine($ "TEST NULLABLE B -> A0 (result):  $r4")
WriteLine($"TEST NULLABLE B -> A0 (type):  $t4" )
// run-time messages: OK, this is what I expected.
// TEST NULLABLE B -> A0 (result):  1
// TEST NULLABLE B -> A0 (type):  System.Int32


def r5 = test(inull_n) // this is a macro declared near ?? macro
def t5 = getType(r5)
WriteLine($"TEST NULLABLE B -> A0 (result):  $r5" )
WriteLine($"TEST NULLABLE B -> A0 (type):  $t5" )
// run-time messages: same result should be in *(++)* test.
// TEST NULLABLE B -> A0 (result):  2
// TEST NULLABLE B -> A0 (type):  System.Int32

def r5 = inull_n ?? CI() // *(++)*compile-time message: "nullable: B -> A0:
if (inull_n != null) inull_n.Value; else (CI () : int)"
// Compile-time exception:
//test.n:59:21:59:23: debug: Internal compiler error, please report a bug to
bugs.nemerle.org. You can try modifying program near this location.
//□[01;31merror□[0m: internal compiler error: assertion failed in file
ncc\generation\ILEmitter.n, line 801:
//_N_AutoModule::Main: failed to convert non-value type CI to a value type
int
// .............

def r5 = if (inull_n != null ) inull_n.Value; else (CI () : int)
def t5 = getType(r5)
WriteLine($ "TEST NULLABLE B -> A0 (result):  $r5" )
WriteLine($"TEST NULLABLE B -> A0 (type):  $t5")
// run-time messages: same result should be in *(++)* test.
// TEST NULLABLE B -> A0 (result):  2
// TEST NULLABLE B -> A0 (type):  System.Int32

Look at comments in the tests. They discover the macro problems. Please find
a bit of time and advice me on problem I have. First of all, I want to know,
why macro has a such behaviour in first and second test cases.

==
Best Regards,
  Ivan A. Eryshov.



On 9/23/06, Michal Moskal <michal.moskal at gmail.com > wrote:

> On 9/22/06, Ivan A Eryshov < ivan.eryshov at gmail.com> wrote:
> >
> > On 9/22/06, Michal Moskal wrote:
> > >If C# 3.0 has it, I'm for including the macro mentioned by Ivan in the
> > >standard library.
> > >
> > >As far as I understand it should also work with nullable types?
> >
> > Yes, this operator should work with nullable types too. Just today I had
> > start write macro which is meet C# specification on '??' operator. I
> plan
> > find some time and finish them tomorrow or day after tomorrow.
>
> In fact I think that your macro (as it stands now) should work with
> nullable types, because in C# 2.0 the expression foo != null would be
> transformed to foo.HasValue if foo is of nullable type. Therefore
> similar thing should also happen in Nemerle. I'm not sure how easy to
> implement it though.
>
> --
>   Michał
>
> _______________________________________________
> https://nemerle.org/mailman/listinfo/devel-en
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: /mailman/pipermail/devel-en/attachments/20060924/bf42a383/attachment-0001.html


More information about the devel-en mailing list