dotnet / csharplang

The official repo for the design of the C# programming language
11.32k stars 1.02k forks source link

Champion "Replace/original and code generation extensions" #107

Open gafter opened 7 years ago

gafter commented 7 years ago

See also

masaeedu commented 7 years ago

I forgot to mention why that is useful; the implementations are supplied by IL-rewriting or Roslyn snuck into the msbuild or whatever.

Richiban commented 7 years ago

Since AOP seems to mostly cover what people are trying to achieve with source generators, would it be better to instead support a simpler implementation that supports 90% of the cases? I'm thinking method & class decorators, a bit like those in ECMAScript.

From the IDE experience I imagine that the emitted code would be visible at the beginning & end of the decorated method, but would be collapsed in a region-like fold (and would, of course, be read-only).

I'm also really comfortable with a tool that emits source code actually to disk in a different file. What do you do with it? Do you check it in? If not, it sounds like it would be mighty easy to have builds that work / don't work from one machine to the next due to minor differences in tooling.

gulshan commented 7 years ago

Are we trying have something like Java annotation processor?

Pzixel commented 7 years ago

@Richiban it sounds cool when you want to write some decorators but it's completely useless when you want to rewrite methods. Common example - Steno which transforms heavy LINQ into lighweight procedural code. For example, there is Shaman LINQ which is working pretty well, but I'd like to have it such as compiler's pipeline module instead of single handler. We may want to chain two rewriting modules (for example, LINQ rewriter and Loops-to-SIMD rewriter), but today we just are not able to do it. With this feature existing, we could just get two compiler-modules as we are getting roslyn analyzers and be golen. It looks like LLVM modules transofming the code.

@CyrusNajmabadi

If you want something without IDE support, then it's really not that hard. just add build steps that invoke some tool of yours to process your code (using the Roslyn APIs) and then just emit your in memory trees that you've generated.

We want IDE support in terms of being able to click F5 and have all handlers being called. It's good enough for MVP to be non-debuggable non-viewable etc etc... But it still true for Roslyn Analyzers - you cannot see what code produces the error, the same way you don't see how some piece of code is generated (and you don't see it in IDE, only in debugger). The MVP we need is Fody/PostSharp analogue, which is working with source code, not with binaries (you never want to compile code and then get it immediately decompiled) and may contains multiple modules (rewriters for A, rewriters for B, etc...). You even may forbid these analyzers to change public API for first version, that opens doors for rewrites that optimizes internals, thus you don't have to analyze anything except you are already do. And it still be valuable.

masaeedu commented 7 years ago

I'd like to have it such as compiler's pipeline module instead of single handler. We may want to chain two rewriting modules (for example, LINQ rewriter and Loops-to-SIMD rewriter), but today we just are not able to do it

@Pzixel Actually, you can chain as many IL-rewrite steps into your build as you want. Simply adjust your MSBuild accordingly (or use something like Fody if you don't like MSBuild). The only thing you aren't able to achieve is getting proper feedback in the type system.

Could you elaborate on what you mean by "compiler's pipeline module instead of single handler"?

Pzixel commented 7 years ago

@masaeedu LLVM pipeline looks similar to what I am talking about. But let's take Roslyn compiler-pipeline-api 1

I imagine it like this image

For example, I want every string in programm to be prefixed with "Hello, ". So I just write a roslyn visitor that is replacing every node with constant string X with "Hello, " + X. It's just like how refactorings are currently working, except that we don't need user to click apply refactoring, we just want it to performed automatically when sources are compiled.

Could you elaborate on what you mean by "compiler's pipeline module instead of single handler"?

Nowadays, it's possible to do it automatically by writing your own Roslyn-based-compiler, which will replace some code before calling MSBuild (see Shaman LINQ above for reference). But it works because author of this library copy-pasted some internal MS structures and compiles itself. When all we need is a simple visitor. And as you can see, 95% of this library is just duplicating of Roslyn functionality. This is what I call Single handler - you can pass sln file to be compiled to one util only. But with modules in pipeline they can be, well, chained together :) This is what I call modules. It's similar to ASP.Net modules and handlers - you can have lots of modules in pipeline, but handler is always single.

masaeedu commented 7 years ago

@Pzixel

For example, I want every string in programm to be prefixed with "Hello, ". So I just write a roslyn visitor that, just like how refactorings are currently working

Yes, and this is possible to do right now. There are no additional features required to support this scenario; see for example https://github.com/AArnott/CodeGeneration.Roslyn, which makes it convenient to add Roslyn rewriters to an assembly's build step.

But it works because author of this library copy-pasted some internal MS structures and compiles itself.

The fact that Shaman LINQ implemented their tool in a particular way isn't an inherent restriction of the Roslyn compiler API or the build system. What you show in the diagram; i.e. the ability to perform multiple passes of transformation on the AST, is currently possible; you just need to insert it into the right part of the build process. In addition, it is also possible to do an arbitrary number of passes of IL-rewriting after the IL is emitted; in fact all the tools we use today like Fody/PostSharp etc. already do this.

Pzixel commented 7 years ago

@masaeedu sounds good, I'l take a look asap and come back with results. But as far as I see it requires some help from code itself: defining custom attributes etc.. Probably it's not required, but it's true for an example at least.

mattwar commented 7 years ago

While possible, its not at all convenient to extend the binder phase, because the bound tree and all related logic is not exposed publicly.

orthoxerox commented 7 years ago

I find the syntax tree somewhat annoying to manipulate. There are lots of places in the compiler where it demands the original syntax tree. Sometimes it checks that the node is a part of the method body syntax tree, sometimes it goes up and checks the parent. You have to tread really carefully around it.

For example, right now I'm trying to implement take until as a query expression. Since the standard method is TakeWhile, I did the obvious thing and wrapped the condition in a PrefixUnaryExpression. Of course, there's a place in the compiler where it checks for the parent node to check if we're in LINQ and you can't set it directly, so I jumped through some hoops:

takeOrSkip = takeOrSkip
    .WithExpression(
        SyntaxFactory.PrefixUnaryExpression(
            SyntaxKind.LogicalNotExpression, expr))
    .WithWhileOrUntilKeyword(
        SyntaxFactory.Token(SyntaxKind.WhileKeyword));
expr = takeOrSkip.Expression;

and then of course there's Microsoft.CodeAnalysis.CSharp.Symbols.SourceMethodSymbol.CalculateLocalSyntaxOffset(Int32 localPosition, SyntaxTree localTree) that trips you up next. And of course, this being LINQ, it's the only place where you cannot do this later via lowering.

I now cannot help but wonder how replace/original will handle this. Will it require a new compilation step between parsing and binding, so the binder gets a new rewritten tree with no stitches showing?

CyrusNajmabadi commented 7 years ago

Will it require a new compilation step between parsing and binding, so the binder gets a new rewritten tree with no stitches showing?

The original design was that you had two compilations. The one before hte rewrite, and the one after. Precisely so that everything would work properly and there would be no need to know anything about how the compiler worked internally.

CyrusNajmabadi commented 7 years ago

Since the standard method is TakeWhile, I did the obvious thing and wrapped the condition in a PrefixUnaryExpression

FYI, none of the compiler works this way. We don't implement language features as a rewrite of the syntax. Instead, the binder tries to initially be a close representation of source. Then the binder's nodes are rewritten through the act of 'lowering'.

orthoxerox commented 7 years ago

@CyrusNajmabadi I know that, but LINQ is the exception. It's rewritten into lambdas during the binding phase, there's no BoundQueryClause.

alrz commented 7 years ago

@orthoxerox re "there's no BoundQueryClause", there is. see MakeQueryClause. But you're right it's lowered during binding, LocalRewriter_Query simply visits the inner nodes. However, that's irrelevant here, you'd rewrite ast (just like analyzers) not bound nodes.

orthoxerox commented 7 years ago

@alrz It's a good thing I didn't bet to eat my hat, isn't it?

mattwar commented 7 years ago

@orthoxerox replace/original is a different feature than source generation. Replace/original just talks about how one method can be defined to replace the declaration of an separate method. You can technically use this with out any source generation, it probably does not make sense to. You can also use it with existing design time tool source generation just like you can with partial methods. Of course, it was intended to be used with source generation, but the scenario is when you generate new source files, not rewriting existing syntax trees. The canonical example is one where the replaced method invokes the original method, so it acts like a wrapper around the original users code. However, there is also a scenario for not calling the users code. It is possible you could replace the original declaration with an entirely different one, or one based on the original body, but changed in some way. That would probably be the closest to what you are trying to do, but not really the core scenario for replace/original.

Pzixel commented 7 years ago

@mattwar I don't understand what you are talking about. I don't see why replacement with just a decorator have to differ from completly different code based on user's one. And it's definitely useful case.

The canonical example is one where the replaced method invokes the original method, so it acts like a wrapper around the original users code.

I don't see any canonicity here. Just one use case which isn't even the most demanded. Code rewriter just have to rewrite an AST (obviously), it has no requirements about how many percent of user code it must reuse.

vbcodec commented 7 years ago

@mattwar What about replace/original feature implemented vertically than horizontally ? By vertical implementation I mean with help of inheritance.

Class Base
Sub Sub1 ()
End Sub
End Class

Public Class Derived
Inherits Replace Base

Sub Overrides OnEnter Sub1()
End Sub

Sub Overrides OnExit Sub1()
End Sub

Sub Overrides OnError Sub1(Ex As Exception)
End Sub

End Class

Dim o = new Base ' Really create object of Derived, because it is most recent derivative of Base class marked with Replace modifier

These Derived.S1 functions marked with OnEnter , OnExit and, OnError modifiers are called when Base.Sub1 is called, and are implicitly inheritable (you may imagine that they exist in Base class).

mattwar commented 7 years ago

@Pzixel Neither replace/original language feature and the source generation proposal are intended to allow for tree's to be rewritten. Source generators explicitly generate new source files, they do not modified existing ones or the trees produced from them. Replace/original is a technique to allow you to replace the body of an existing method with different code at the level of the language itself, all without rewriting trees.

jahmai-ca commented 7 years ago

@mattwar A number of interesting and desirable use-cases had their own issues closed and redirected to point to this issue. It is not fair to state the intention of this issue doesn't cover those cases. The only reason I am here is to encourage something like the code injected decorator case, ala AOP / PostSharp. If that's not appropriate to discuss in this issue then the other issues should be re-opened.

mattwar commented 7 years ago

@vbcodec, there is already a vertical means of replacing a function, its called virtual methods.

mattwar commented 7 years ago

@jahmai I don't want to stifle discussion, so go ahead and discuss whether this proposal should have additional capabilities, but maybe not go too far as to hijack the discussion.

Pzixel commented 7 years ago

@mattwar please, explain, how it it possible to replace method body without modifying its AST? I'd appreciate some example.

amis92 commented 7 years ago

@Pzixel there already is an example in spec. To completely replace and not use original definition, just don't call original in replacing declaration, say

replace void F() { System.Console.WriteLine(); }
Pzixel commented 7 years ago

@amis92 I see two problems here:

  1. It replaces methods with known name only. So it's hard to write a logging decorator for all methods in all classes, for example.
  2. We aren't able to modify part of method. For example if I want to count lines of method and I want to place i++ between all lines in method. I just cannot use original/replace keywords to change
    public static int Foo()
    {
    int x = 10;
    x += 20;
    return x;
    }

    to

    public static int Foo()
    {
    int loggerCounter = 0;
    int x = 10;
    loggerCounter++;
    x += 20; 
    loggerCounter++;
    return x;
    }
amis92 commented 7 years ago

@Pzixel

  1. "Hard" is not really a good description - writing any analyzer is "hard" because it requires knowledge of Roslyn APIs. But when you already know that, writing analyzer (or generator for that case) that replaces every method with a rewritten method would be no harder that writing any other.

  2. You're too focussed on "modifying". If you have the ability to completely replace any given method while having access to source code of original (and by the spec, you do), there isn't really any problem with doing what you want. In generated file just add replace modifier to Foo 's declaration and done. Of course, your generator would have to "generate" that whole method body, but thanks to Roslyn APIs that would be semi-trivial.

Pzixel commented 7 years ago
  1. Hard seems to be inappropriate word here, I meant I have no idea how to do it with these keywords. I'm pretty familiar with Roslyn API and I see how it may look if it works like current analyzers.
  2. I think we need some prototype here to continue discussion. I'm focussed on "modifying" because I imagine pretty well how generation of completly new code should workd, and I agree with position of collaborators above, but I'm not this sure about modification part. So I'm talking about what I see problem in :)
HaloFour commented 7 years ago

@Pzixel

You can't do it with these keywords alone. They only provide the interjection point. You'd need to have the analyzer/generator completely rewrite the method, using replace to have your version override the existing version. For example, say you have the following class:

public partial class Foo {
    public void SayIt() {
        Console.WriteLine("Foo");
        Console.WriteLine("Baz");
    }
}

But you wanted the method to output Baz between Foo and Bar. You'd have to have your analyzer emit the following:

public partial class Foo {
    public replace void SayIt() {
        Console.WriteLine("Foo");
        Console.WriteLine("Bar");
        Console.WriteLine("Baz");
    }
}

You'll note the lack of the use original, because you're never calling the original version of that method. You're replacing it entirely. It's up to the analyzer/generator to take the syntax tree representing the code of the original method and to convert that back into source, interjecting your own source where appropriate. I don't know how that's done but I'll make the wild guess that it wouldn't be appreciably more difficult than writing an analyzer with a code fix.

Pzixel commented 7 years ago

@HaloFour that what I'm talking about. But this proposal with original/replace keywords seems to provide completely different API when some extension for today's Analyser API is required. I have an example linked above, but I'm not sure if it works as expected.

I don't know how that's done

Me too. So I expected that this proposal is about very this issue.

masaeedu commented 7 years ago

@Pzixel FWIW that project (and the general approach of doing transformation as part of the msbuild pipeline) works great for me. Here's what it looks like:

CodeGeneration

I don't really know how the source generator and replace/original features will turn out, or what form they will take, but personally this is good enough for me.

amis92 commented 7 years ago

@Pzixel

But this proposal with original/replace keywords seems to provide completely different API when some extension for today's Analyser API is required.

This proposal is for a language feature which would support Source Generation generators similar to Analysers. Both analysers and generators have to have a great IDE support and both do not aim to support "magic" transformations and things appearing in assembly out of thin air. Analysers provide you with the ability to change your code. Generators provide ability to create new code based on your code. It's essential to this feature that this new source is available to developer in IDE while browsing or debugging original source. No magic, just partial declarations - and maybe these partial declarations replace some methods, maybe just add new, maybe wrap originals.

It's different to what [it seems to me that] you want: hidden optimizations/additions that do not change the way that source sees itself (i.e. new methods not visible in same-project intelligence) and/or do not have any source representation to be debugged/browsed. Am I missing something? If not, as has been pointed above, source transformations are already possible and if you want better support for them, maybe another proposal would be better. It doesn't seem that what you propose has the expected IDE experience that this feature aims at.

LokiMidgard commented 7 years ago

Replace/Original

I would really like to see the see the replace original feature splited from source code generation. While there seems not to be a use case without source code generation, I think there is a good one: source code generation. From this feature would not only benefit Roslyn's own source code generator, that maybe will exist in years, but also every other code generator that outputs source files. It is really easy to write a tool with Roslyn, that puts out some additional source files and invoke it before build. Some time ago I've written a generator that simulated Mixins by coping Methods from one class to another. It was easy because it just added new Methods and Property's, but if I wanted to write a tool that replaces or decorates a Method I would not know how to do it currently without actually rewriting the user code. But I wouldn't call this a code generator, but a Refactoring tool. And while it seems to be hard to address all the issues that arise with the generation of code in the near future, the support of these two keywords seems to be far more feasible. While of course still having its own Problems. (What happens if I replace a Method twice?)


Source Code Generator

For Code Generation itself there are mainly problems with performance if I understood it correctly. (I think it took me over 3 hours to read this thread, could be that I missed, or forgot something)

The problem is for Intellisense to work as expected its reaction time should be < 15 ms. And if code generation takes to long (like 20 sec.) this is not good. It would be cool if compiling would also be fast, but if build took some seconds more, this would not be a critical problem.

Would it be feasible to split source generation in different 'kinds' of source generators? I try to define the usecases that I have in mind.

  1. Only Methods will be replaced using replace/original (e.g. to decorate methods with logging)
    In this case we will not need to invoke the source generator at all when calling Intellisenes, because no new members will be generated, what's inside the methods is not important for intellisense.

  2. New methods, Interface implementation, etc. will be injected, but these are known at compile time or fast calculated (Automatically implement Interfaces like INotifyPropertyChanged)
    I imagine this would also not negatively impact intellisense as long as the Method that list the additional interfaces that a type implements and the additional methods is fast enough. The Member would all have throw new NotImplementedException() as body if a body would be needed. When compiled an more expensive method would be called to created the member body.

  3. New Methods where the creation of the declaration is expensive (Generating Types from Word)
    I think this is the only problematic on. This could maybe bypassed by having a cache that can have multiple entry's which have a trigger when they are no longer valid. Such a trigger could be 'a file was changed' (Word, XML, e.g.), 'Attribute X was removed from class Y'. To update or create a new entry in the cache could take some time, so in this case, per definition, it would not be possible to show accurate and fast information of types. In this case the IDE could display that it is refreshing the cache and it can take some time until all Entry's are available in intellisense.

I'm certain that number one will be no problem and I think number two will also not be that problematic. Of course if the developer thinks he must download data in case 2 it can't be helped, but the IDE could display an Warning like VS does it with plugins if it takes to long. Case 3 is the worst but could be ok if the cache does not needs to be refreshed often and maybe can persisted between sessions.

Of course if you have an expensive operation that needs to be called with every keystroke I'm pretty sure someone can prove it is impossible to make this performant. So I would suggest to not support this case.

tl;dr
Instead of having one general approach to inject code, there are multiple. The expnsiver the calculations to create the new injected code, the complexer the approach will be for the dev to create the generator so it will still be fast in the IDE.

Opiumtm commented 7 years ago

@CyrusNajmabadi It's time to start Visual Studio insider program, just like Windows insider program do. In other words - allow early (beta) access to experimental VS/C# features as simple as it is in Windows insider program. It would be good if "preview" VS could run side by side with "release" VS. So it would satisfy all. Majority of developers would get stable production ready experience. And VS team would get lots of feedback, telemetry, bug reports with new not yet finished features. Windows insider program proves absolute usefulness of public beta testing.

Tragetaschen commented 7 years ago

@opiumtm you do realize that's how VS 2017 currently rolls out?

yaakov-h commented 7 years ago

@Opiumtm allow me to blow your mind: https://www.visualstudio.com/vs/preview/

gulshan commented 7 years ago

This is probably going to be the direction of C++ in this regard- Metaclasses. Elevate coding patterns/idioms into new abstractions built into the language http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0707r0.pdf

Pzixel commented 7 years ago

@masaeedu after several days of work I can say that it works, and works wonderful (except it has some unwanted dependency, but who cares?). So, generate new files during comilation seems to be already possible. Thank you for a link.

However, the problem with replace original content of methods still very valuable. The most common case: implementation of null-guards, ConfigureAwait(false), INotifyPropertyChanging... Everything that needs to replace original code. Just check Fody or PostSharp. They have lots of usages, we can write plugins to not write our code manually. I had two projects, one is handled by this tool linked above. The second one - Steno implementation via Roslyn require this feature to be a bit more powerful. I even can agree that we can move it on N+1 release, but I'm absolutely sure that it have to be implemented, and sooner is better.

@amis92

It's different to what [it seems to me that] you want: hidden optimizations/additions that do not change the way that source sees itself (i.e. new methods not visible in same-project intelligence) and/or do not have any source representation to be debugged/browsed. Am I missing something? If not, as has been pointed above, source transformations are already possible and if you want better support for them, maybe another proposal would be better. It doesn't seem that what you propose has the expected IDE experience that this feature aims at.

It can be debugged because

  1. You can see final code in decompiler. CLR have to see these changes, so why don't have you?
  2. We can just add option to see final result just like precompiled Razor views. Just add class1.cs.debug for every class1.cs which got transformed and you can see its content. Another question is about debugging expierence. We need some investigation here. But here we can see how people are working with tools like mentioned Fody. For example when you debug some application and discovery mechanism cannot find corresponding cs file it asks you Please, gimme the path to sources. I imagine that same mechanism can be applied here, at least as MVP.
BalassaMarton commented 6 years ago

What do we need to do to get this some traction in the community? IMHO this feature - even without the replace/original keywords, just to allow custom code rewrites during build - would be a game changer comparable to generics or async/await. We are already using runtime code generation (with Roslyn or just Reflection.Emit) for reducing repetitive coding and eliminating reflection, and moving those code generation into the build process would not only make it much more easy to implement aspects, mapping classes, etc. but it would increase runtime performance as well.

gulshan commented 6 years ago

I think I have managed to write my idea about how code generation should look like in C#. It is influenced by the C++ generator proposal. Also I have incorporated what I thought code generation should look like in a previous comment. It is actually interpolation of generative code and code templates. In brief, a new modifier template before class means it is a template class and contains generative codes. Generative code sections starts with $ and ends with the scope of the symbol. Withing generative code sections, double ticks ` starts and ends code templates, which may again contain symbols from generative section prefixed with$`. Here is probably the simplest example, a JSON serializer. The actual code according to current proposal can be found here.

public template class JsonSerializer<T> : T
{
    public void Serialize(TextWriter writer)
    {
        const string indent = "    ";

        writer.WriteLine("{");

        ${
            using System.Linq;

            var type = ``T``.GetNamedTypeSymbol()
            var publicProps =  type.GetMembers()
                .Where(m => m.Kind == SymbolKind.Property
                        && m.DeclaredAccessibility == Accessibility.Public);

            foreach (IPropertySymbol prop in publicProps)
            {
                var property = prop.Name;
        ``

        writer.Write(indent);
        writer.Write('"' + nameof($property) + '"');
        ``
                if (prop.Type.SpecialType == SpecialType.System_String)
                {
        ``
        writer.WriteLine(": \"" + $property + "\",");
        ``
                }
                else
                {
        ``
        writer.WriteLine(": " + $property + ",");
        ``
                }
            }
        }

        writer.Write('}');
    }
}

Here is how the generated code will look like for a simple class-

public class Poco
{
    public int TestInt { get; set; }
    public string TestString { get; set; }
}

public class JsonSerializer<Poco> : Poco  // generated class
{
    public void Serialize(TextWriter writer)
    {
        const string indent = "    ";

        writer.WriteLine("{");

        writer.Write(indent);
        writer.Write('"' + nameof(TestInt) + '"');
        writer.WriteLine(": " + TestInt + ",");

        writer.Write(indent);
        writer.Write('"' + nameof(TestString) + '"');
        writer.WriteLine(": \"" + TestString+ "\",");

        writer.Write('}');
    }
}

This is not text/file generation. Rather it is razor like code interpolation. So, tools should be able to provide full support of editing template classes like real-time error handling and intellisense. I envision generative and template parts having different background colors to show differentiation of contexts. The tooling/IDE concerns- IntelliSense, Navigation, Debugging, Project-System, Refactoring, all should work intuitively from other files. User may not see the generated code during navigation, but during debugging, generated code can be shown (with an indicator that it is actually generated). Any comments?

Some changes: I think classes should not have the extension point for generators/templates. Rather the template class should be able to derive from the generic type like- public template class JsonSerializer<T> : T. Then the class behavior remains same. When user needs a class with generated JsonSerializer, they have to use the type JsonSerializer<Poco> for object creation. Thus generators will work with existing types without breaking their functionality. I have changed my example code accordingly.

Pzixel commented 6 years ago

@gulshan It really looks like T4. And it doesn't fit original spec because you have to specify templte keyword and extension points in your class while original proposal allows to modify classes that have no idea that they would be changed

LYP951018 commented 6 years ago

I'm curious how type providers in F# works? Does F# faces these tooling problems too?

LYP951018 commented 6 years ago

Could we just constrain the running time of generators to solve the intellisense speed issue (partially)?

gulshan commented 6 years ago

@Pzixel It does not match the original proposal because it was not the intention. The intention is to imagine how code generation in C# may look like given the original proposal has very little chance to move forward. And regarding the extension point, the JsonSerializer example I followed used the same extension point.

Pzixel commented 6 years ago

@gulshan we already have T4 generators. See example from my repo:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="$(SolutionDir)ZLinq.TTHelp\bin\$(Configuration)\ZLinq.TTHelp.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="ZLinq.TTHelp" #>
<#@ output extension=".cs" #>

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;

namespace ZLinq
{
    public static partial class ZEnumerable
    {        
        <# foreach (var sourceType in TT.WithNonGen(TT.StandardCollections))
        { 
        #>[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
        [ContractAnnotation("source:null => true; source:notnull => false")]
        public static bool IsNullOrEmpty<#= TT.GetConstraint(sourceType) #>([CanBeNull] this <#= sourceType #> source)
        {
            return source == null || source.<#= TT.LengthOrCount(sourceType)#> == 0;
        }

        <#}
        #>
    }
}

becomes

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;

namespace ZLinq
{
    public static partial class ZEnumerable
    {        
        [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
        [ContractAnnotation("source:null => true; source:notnull => false")]
        public static bool IsNullOrEmpty<T>([CanBeNull] this T[] source)
        {
            return source == null || source.Length == 0;
        }

        [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
        [ContractAnnotation("source:null => true; source:notnull => false")]
        public static bool IsNullOrEmpty<T>([CanBeNull] this List<T> source)
        {
            return source == null || source.Count == 0;
        }

        [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
        [ContractAnnotation("source:null => true; source:notnull => false")]
        public static bool IsNullOrEmpty<T>([CanBeNull] this IList<T> source)
        {
            return source == null || source.Count == 0;
        }

        [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
        [ContractAnnotation("source:null => true; source:notnull => false")]
        public static bool IsNullOrEmpty<T>([CanBeNull] this ICollection<T> source)
        {
            return source == null || source.Count == 0;
        }

        [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
        [ContractAnnotation("source:null => true; source:notnull => false")]
        public static bool IsNullOrEmpty([CanBeNull] this IList source)
        {
            return source == null || source.Count == 0;
        }

        [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
        [ContractAnnotation("source:null => true; source:notnull => false")]
        public static bool IsNullOrEmpty([CanBeNull] this ICollection source)
        {
            return source == null || source.Count == 0;
        }
    }
}

You just propose the same thing with slightly different syntax.

gulshan commented 6 years ago

Yes it looks like T4 templates. But it is integrated into the language. So the compilation is a single step process. Actually no new "code" is generated. What being generated is IL which is result of compilation. This can even be in-memory as @jnm2 once proposed. So, the code check-in/project inclusion issue is not there. I have changed the proposal a bit. So, it would not alter the actual class. Rather it will instantiate a new class on demand like generic classes. Intellisense should be ok as parent and generated classes are separate. So, it is trying to alleviate the issues T4 templates or the current proposal have.

Pzixel commented 6 years ago

@gulshan probably it's better to enhance T4 and its support in VS instead of implementing yet another code generator?

jahmai-ca commented 6 years ago

I don’t see how this code template suggestion solves the AOP code injection case for things like argument validation?

gulshan commented 6 years ago

@jahmai The generative blocks do not have to be within a method. They can be outside the method and thus generate methods. The generative blocks are confined just within the template class. AOP/code injection will look almost like Pzixel's T4 template code above.

LokiMidgard commented 6 years ago

I never worked with T4, so correct me if I'm wrong.

But for me the samples looks like I can use T4 with one of my own classes to save some typing (and maybe copy paste code, which is always good). But I can't use a library that implemented something I now just can use.

Let's assume someone else wrote a Library that let you easily implement INotifyPropertyChanged

In the original proposal you could have done something like this:

class Foo {

    [ImplementPropertyChanged]
    public string Bar { get; set; }
}

And the generator could add the interface, implement it and if replace would exist, add the Notification code to the property Bar.

Using the T4 approach I have to copy the actual code that will generate the needed functionality in every class that want to use it. I assuming that the replace keyword also exists in this case, so T4 can replace the actual properties.

Because I don't know T4 enough I'll skip the code sample.


In a Nutshell, what I would like to see is: adding a nuget Package, maybe adding some attributes to control what needs to be done, and the magic happens.

It would also not need to be reduced on C# files, you can take xml as input and get classes as output. Like a SoapClient generator.

Pzixel commented 6 years ago

@LokiMidgard T4 is defenitly not replacement for this proposal. It was just an answer to @gulshan 's comment. T4 may save typings but it generally unaware of what's outside his *.tt file.