dotnet / csharpstandard

Working space for ECMA-TC49-TG2, the C# standard committee.
Creative Commons Attribution 4.0 International
710 stars 84 forks source link

LINQ: Select Clause #688

Open bernd5 opened 1 year ago

bernd5 commented 1 year ago

In 11.18.3.6 Select clauses the spec says that

from c in customers.Where(c => c.City == "London")
select c

is translated to

(customers).Where(c => c.City == "London")

But the current reference implementation translates it to (ignoring delegate caching and extension method expansion):

customers.Where(c => c.City == "London").Select(c => c)

-> The Select call is not omitted

Only for queries like:

from c in customers
where c.City == "London"
select c

the Select call is omitted.

Is this a spec or roslyn bug?


To make it work, compilers would need to analyze invocation expressions and "expand" the original Linq query first - which is not done in roslyn.

KalleOlaviNiemitalo commented 1 year ago

Microsoft (R) Visual C# Compiler version 4.8.4084.0 for C# 5 (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe in .NET Framework 4.8) likewise emits an Enumerable.Select call for the following source code:

using System;
using System.Collections.Generic;
using System.Linq;

static class C
{
    static IEnumerable<int> M(IEnumerable<int> source)
    {
        return from i in source select i;
    }
}

How about Mono's C# compiler?

bernd5 commented 1 year ago

I compiled the following code with mcs / mono (Mono C# compiler version 6.12.0.182). The result is similar to roslyn (Select is not omitted for Test1 but for Test2):

Source:

using System;
using System.Linq;
using System.Collections.Generic;

class App{
    public static void Main(){
        Customer[] customers = new[]{
            new Customer(){
            }
        };
        Test1(customers);
        Test2(customers);
    }

    static IEnumerable<Customer> Test1(IEnumerable<Customer> customers){
        return from c in customers.Where(c => c.City == "London")
        select c;
    }

    static IEnumerable<Customer> Test2(IEnumerable<Customer> customers){
        return from c in customers
        where c.City == "London"
        select c;
    }
}

class Customer{
    public string City {get; set;}
}

Decompiled mcs / mono code (ILSpy):

// App
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

internal class App
{
    [CompilerGenerated]
    private static Func<Customer, bool> <>f__am$cache0;

    [CompilerGenerated]
    private static Func<Customer, Customer> <>f__am$cache1;

    [CompilerGenerated]
    private static Func<Customer, bool> <>f__am$cache2;

    public static void Main()
    {
        Customer[] customers = new Customer[1]
        {
            new Customer()
        };
        Test1(customers);
        Test2(customers);
    }

    private static IEnumerable<Customer> Test1(IEnumerable<Customer> customers)
    {
        if (<>f__am$cache0 == null)
        {
            <>f__am$cache0 = new Func<Customer, bool>(<Test1>m__0);
        }
        IEnumerable<Customer> source = Enumerable.Where(customers, <>f__am$cache0);
        if (<>f__am$cache1 == null)
        {
            <>f__am$cache1 = new Func<Customer, Customer>(<Test1>m__1);
        }
        return Enumerable.Select(source, <>f__am$cache1);
    }

    private static IEnumerable<Customer> Test2(IEnumerable<Customer> customers)
    {
        if (<>f__am$cache2 == null)
        {
            <>f__am$cache2 = new Func<Customer, bool>(<Test2>m__2);
        }
        return Enumerable.Where(customers, <>f__am$cache2);
    }

    [CompilerGenerated]
    private static bool <Test1>m__0(Customer c)
    {
        return c.City == "London";
    }

    [CompilerGenerated]
    private static Customer <Test1>m__1(Customer c)
    {
        return c;
    }

    [CompilerGenerated]
    private static bool <Test2>m__2(Customer c)
    {
        return c.City == "London";
    }
}
bernd5 commented 1 year ago

@KalleOlaviNiemitalo I think the emitting of Select is correct in your sample because of: degenerate-query-expressions

KalleOlaviNiemitalo commented 1 year ago

Okay but then isn't this also a degenerate query expression?


from c in customers.Where(c => c.City == "London")
select c

In which case the standard contradicts itself.

bernd5 commented 1 year ago

That is my point. That is not really a contradiction, but current compilers do not recognize the method-syntax in expression e as part of the query... This works only for pure query syntax.

bernd5 commented 1 year ago

Therefore I think just the sample needs to be corrected from:

    from c in customers.Where(c => c.City == "London")
    select c

    is simply translated into

    (customers).Where(c => c.City == "London")

To:

    from c in customers
    where c.City == "London"
    select c

    is simply translated into

    (customers).Where(c => c.City == "London")
svick commented 1 year ago

current compilers do not recognize the method-syntax in expression e as part of the query

Is there anything in the spec that says they should?


Therefore I think just the sample needs to be corrected [...]

The sample is technically correct in the context of what the spec says at the start of the section:

A query expression is processed by repeatedly applying the following translations until no further reductions are possible. The translations are listed in order of application: each section assumes that the translations in the preceding sections have been performed exhaustively, and once exhausted, a section will not later be revisited in the processing of the same query expression.


[...] needs to be corrected [...] to

The suggested code does not fit the pattern from «x» in «e» select «v».

bernd5 commented 1 year ago

Is there anything in the spec that says they should?

The sample which I refer to says that:

from c in customers.Where(c => c.City == "London")
select c

is translated to

(customers).Where(c => c.City == "London")

but it is translated to

(customers).Where(c => c.City == "London").Select(x => x)

Better sample:

from c in (
    from x in customers
    where x.City == "London"
    select x
)
select c

The inner query:

from x in customers
where x.City == "London"
select x

is translated to:

from x in customers.Where(x=> x.City == "London")
select x

Then to:

customers.Where(x => x.City == "London")

So we get:

from c in customers.Where(x => x.City == "London")
select c

Which is further translated to:

customers.Where(x => x.City == "London").Select(x => x)