Skip to content

Add Template Generator#1

Draft
InsanityCode wants to merge 35 commits into
chaosfrom
add-template-generator
Draft

Add Template Generator#1
InsanityCode wants to merge 35 commits into
chaosfrom
add-template-generator

Conversation

@InsanityCode

Copy link
Copy Markdown
Member

Template Generator

This source generator allows generating multiple overloads for a method by providing its signature and a generic implementation, similar to the concept of C++ templates.

The goal of this generator is to drastically reduce copy paste code and documentation for methods that can't be easily implemented using normal generics, like methods that need to use arithmetic or logical operators, which cannot be required by generic type constraints, like in ChaosFramework.Math's Min, Max or Clamp methods.

How to use

Define the signature of the method to be generated by declaring a generic delegate. The generic parameters of the delegate will serve as template type parameters. Then decorate the delegate with a TemplateGeneratorAttribute. The first argument of the attribute constructor is the string to be used for the implementation. The second argument specifies the types (or type combinations) for which to implement the template. Within the implementation string occurences of the generic type parameters' names will be replaced by the actual types for which to implement the template. It is possible to generate generic templates by providing null for specific template type parameters.

Example

The following code declares a template method named Test:

namespace Example.Namespace
{
    public static partial class ExampleClass
    {
        internal partial struct ExampleStruct
        {
            /// <summary>
            ///     Hello, World!
            /// </summary>
            [ChaosGenerators.TemplateGenerator(
                "=> new System.Tuple<T1, T2, T3>(v1, v2, v3[0]);",
                typeof(float), typeof(int), typeof(string),
                typeof(float), null, typeof(string),
                null, typeof(int), null
                )]
            internal delegate System.Tuple<T1, T2, T3> _Test<T1, T2, T3>(ref T1 v1, ref readonly T2 v2, params T3[] v3);
        }
    }
}

This results in the following code being generated:

// <auto-generated/>

namespace Example.Namespace
{
    public static partial class ExampleClass
    {
        internal partial struct ExampleStruct
        {
            /// <summary>
            ///     Hello, World!
            /// </summary>
            internal static System.Tuple<float, int, string> Test(ref float v1, ref readonly int v2, params string[] v3)
            => new System.Tuple<float, int, string>(v1, v2, v3[0]);

            /// <summary>
            ///     Hello, World!
            /// </summary>
            internal static System.Tuple<float, T2, string> Test<T2>(ref float v1, ref readonly T2 v2, params string[] v3)
            => new System.Tuple<float, T2, string>(v1, v2, v3[0]);

            /// <summary>
            ///     Hello, World!
            /// </summary>
            internal static System.Tuple<T1, int, T3> Test<T1, T3>(ref T1 v1, ref readonly int v2, params T3[] v3)
            => new System.Tuple<T1, int, T3>(v1, v2, v3[0]);
        }
    }
}

@InsanityCode InsanityCode added draft This is a draft pull request. Do not merge (yet). enhancement New feature or request labels May 31, 2024
@InsanityCode InsanityCode self-assigned this May 31, 2024
@InsanityCode

Copy link
Copy Markdown
Member Author

@FramePerfection

While there are still some TODOs to be resolved (hence the https://github.com/ChaosTechnology/ChaosGenerators/labels/draft label) this should present the basic concept. So, please take a look and tell me what you think.

The above mentioned math methods can be implemented like this:

/// <summary> Returns the greatest of the given values. </summary>
/// <param name="a"> First value to be compared. </param>
/// <param name="b"> Second value to be compared. </param>
[TemplateGenerator(
    "=> a > b ? a : b;",
    typeof(sbyte), typeof(byte),
    typeof(short), typeof(ushort),
    typeof(int), typeof(uint),
    typeof(long), typeof(ulong),
    typeof(float), typeof(double), typeof(decimal)
    )]
public delegate T _Max<T>(T a, T b);

@FramePerfection FramePerfection left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general approach is looking good so far.

I'm not sure where exactly in the "build hierarchy" I'd see this generator concept (or this generator in particular), but since it depends on L1 libraries, I don't think it should be outside of the scope of the hierarchy system like ChaosPresets and ChaosReferences are.
My intuition says that this is L2 - ChaosBuild, as any generator is a necessary part of the build chain and invoked by the build tools, but there may very well be technical reasons against putting "general" generators in that layer. They are, after all, quite different from build tasks.

return;
}

string templateMethodName = templateFileName.TrimStart('_');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TrimStart makes it possible to create templates that will generate code that will not compile, particularly by using the same template name with a different amount of underscores, e.g. delegate int _Test<T>() and delegate int __Test<T>().
It may be better to demand an exact number of underscores or some other exact prefix to prevent this particular issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ability to produce methods with the same name is the exact reason why a variable number of underscores (or any other distinction really) is necessary. After all two delegates may not share the same name, even if their parameters differ.

E.g. Max:

/// <summary> Returns the greatest of the given values. </summary>
/// ...
[TemplateGenerator(
    "=> a > b ? a : b;",
    typeof(sbyte), typeof(byte), ...
    )]
public delegate T __Max<T>(T a, T b);

and

/// <summary> Returns the greatest of the given values. </summary>
/// ...
[TemplateGenerator(
    "{\n    foreach (T b in v)\n        v0 = Max(v0, b);\n    return v0;\n}",
    typeof(sbyte), typeof(byte), ...
    )]
public delegate T ___Max<T>(T v0, params T[] v);

And yes, obviously you can generate code that will not compile.
Just like you can write such code by hand...

code.AppendLine(Indent(--indent, "}"));

context.AddSource(
$"{containingType.FullName()}.{templateFileName}.cs",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to raise an InvalidArgumentException here by declaring different overloads for delegates with the TemplateGenerator attribute and the same name. This should be reported as a diagnostic instead, I think.
I don't really understand why I'm still getting the exception in the build log when I wrap it in a try/catch block, however.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, this should raise an error.

For why you still get the exception...

  • some other issue may have prevented the generator from recompiling
  • the old binary may have been cached somewhere

Both happened a couple times for me.
In that case

  1. Close the IDE
  2. Delete the build folders for the generator and the project referencing it
  3. Restart the IDE

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took care of that: c44c359
No error. Instead a unique file name as both of the following produce valid non conflicting implemenations.

internal delegate System.Tuple<T1, T2> _Test<T1, T2>(ref T1 v1, ref readonly T2 v2);
internal delegate System.Tuple<T1, T2, T3> _Test<T1, T2, T3>(ref T1 v1, ref readonly T2 v2, params T3[] v3);


static string GetTypeName(ITypeSymbol type)
{
// TODO: Move to ChaosAnalyzers?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, specifically to TypeUtils.cs

@FramePerfection

Copy link
Copy Markdown
Member

It may be nice to have a constructor for the TemplateGenerator attibute that takes a string of "comma-separated" (blank space will be enough, of course) type names instead of a type array, since such a string could be constructed as a constant.
This would be particularly useful for numeric types, for example, where you wouldn't want to write out every single type for every single template.

Comment on lines +231 to +238
for (int overload = 0; overload < numOverloads; overload++)
{
((Regex, string), ITypeSymbol)[] result = new ((Regex, string), ITypeSymbol)[templateParameters.Length];
for (int i = 0; i < templateParameters.Length; i++)
result[i] = (templateParameters[i], templateArgumentTypes[overload * templateParameters.Length + i]);

yield return result;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop should check for duplicate template argument combinations (i.e. duplicate overloads it would be generating) and raise an error diagnostic in that case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess a warning would suffice, as detecting and skipping such a duplicate would not break anything. But valid point.

@InsanityCode

Copy link
Copy Markdown
Member Author

From Comment

I'm not sure where exactly in the "build hierarchy" I'd see this generator concept (or this generator in particular), but since it depends on L1 libraries, I don't think it should be outside of the scope of the hierarchy system like ChaosPresets and ChaosReferences are. My intuition says that this is L2 - ChaosBuild [...]

I placed it in L1 in my test environment, as for MSBuild a generator and an analyzer are essentially the same. As long as a specific generator does not depend on a higher level library, there's no need to place it anywhere else.
ChaosBuild holds build tasks which are fundamentally different from anything an analyzer or generator does, so this is definitely not the place.

…ad-duplicates-review

diagnose generating overload duplicates - review
…ad-duplicates

Report error diagnostic when a specific overload from a single template would be generated multiple times
@InsanityCode InsanityCode marked this pull request as draft October 18, 2025 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

draft This is a draft pull request. Do not merge (yet). enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants