예제 #1
0
        private static void GenerateModuleSiteDocumentation(KalkModuleToGenerate module, string siteFolder)
        {
            if (module.Name == "KalkEngine")
            {
                module.Name = "General";
            }

            module.Members.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.Ordinal));

            module.Title = $"{module.Name} {(module.IsBuiltin ? "Functions":"Module")}";

            if (module.Name.EndsWith("Intrinsics"))
            {
                module.Title = $"Intel {module.Name.Replace("Intrinsics", "")} Intrinsics";
                module.Name  = $"{module.Name.Replace("Intrinsics", "")}";
            }
            var name = module.Name.ToLowerInvariant();

            module.Url = $"/doc/api/{name}/";

            const string templateText = @"---
title: {{module.Title}}
url: {{module.Url}}
---
{{~ if !module.IsBuiltin ~}}

{{ module.Description }}

In order to use the functions provided by this module, you need to import this module:

```kalk
>>> import {{module.Name}}
```
{{~ end ~}}
{{~ if (module.Title | string.contains 'Intrinsics') ~}}

In order to use the functions provided by this module, you need to import this module:

```kalk
>>> import HardwareIntrinsics
```
{%{~
{{NOTE do}}
~}%}
These intrinsic functions are only available if your CPU supports `{{module.Name}}` features.
{%{~
{{end}}
~}%}

{{~ end ~}}
{{~ for member in module.Members ~}}

## {{member.Name}}

`{{member.Name}}{{~ if member.Params.size > 0 ~}}({{~ for param in member.Params ~}}{{ param.Name }}{{ param.IsOptional?'?':''}}{{ for.last?'':',' }}{{~ end ~}}){{~ end ~}}`

{{~ if member.Description ~}}
{{ member.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
{{~ end ~}}
{{~ if member.Params.size > 0 ~}}

    {{~ for param in member.Params ~}}
- `{{ param.Name }}`: {{ param.Description}}
    {{~end ~}}
{{~ end ~}}
{{~ if member.Returns ~}}

### Returns

{{ member.Returns | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
{{~ end ~}}
{{~ if member.Remarks ~}}

### Remarks

{{ member.Remarks | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
{{~ end ~}}
{{~ if member.Example ~}}

### Example

```kalk
{{ member.Example | regex.replace `^\s{4}` '' 'm' | string.rstrip }}
```
{{~ end ~}}
{{~ end ~}}
";
            var          template     = Template.Parse(templateText);

            var apiFolder = Path.Combine(siteFolder, "doc", "api");

            if (module.Title.Contains("Intel"))
            {
                apiFolder  = Path.Combine(apiFolder, "intel");
                module.Url = $"/doc/api/intel/{name}/";
            }

            // Don't generate hardware.generated.md
            if (name == "hardware")
            {
                return;
            }

            var result = template.Render(new { module = module }, x => x.Name);

            File.WriteAllText(Path.Combine(apiFolder, $"{name}.generated.md"), result);
        }
예제 #2
0
        private static List <KalkModuleToGenerate> FindIntrinsics(Compilation compilation)
        {
            var regexName = new Regex(@"^\s*\w+\s+(_\w+)");
            int count     = 0;

            var intelDoc  = XDocument.Load("intel-intrinsics-data-latest.xml");
            var nameToDoc = intelDoc.Descendants("intrinsic").Where(x => IntrinsicsSupports.Contains(x.Attribute("tech").Value ?? string.Empty)).ToDictionary(x => x.Attribute("name").Value, x => x);

            var generatedIntrinsics = new Dictionary <string, KalkIntrinsicToGenerate>();

            foreach (var type in new Type[]
            {
                typeof(System.Runtime.Intrinsics.X86.Sse.X64),
                typeof(System.Runtime.Intrinsics.X86.Sse),
                typeof(System.Runtime.Intrinsics.X86.Sse2),
                typeof(System.Runtime.Intrinsics.X86.Sse2.X64),
                typeof(System.Runtime.Intrinsics.X86.Sse3),
                typeof(System.Runtime.Intrinsics.X86.Ssse3),
                typeof(System.Runtime.Intrinsics.X86.Sse41),
                typeof(System.Runtime.Intrinsics.X86.Sse41.X64),
                typeof(System.Runtime.Intrinsics.X86.Sse42),
                typeof(System.Runtime.Intrinsics.X86.Sse42.X64),
                typeof(System.Runtime.Intrinsics.X86.Aes),
                typeof(System.Runtime.Intrinsics.X86.Avx),
                typeof(System.Runtime.Intrinsics.X86.Avx2),
                typeof(System.Runtime.Intrinsics.X86.Bmi1),
                typeof(System.Runtime.Intrinsics.X86.Bmi1.X64),
                typeof(System.Runtime.Intrinsics.X86.Bmi2),
                typeof(System.Runtime.Intrinsics.X86.Bmi2.X64),
            })
            {
                var x86Sse = compilation.GetTypeByMetadataName(type.FullName);
                foreach (var method in x86Sse.GetMembers().OfType <IMethodSymbol>().Where(x => x.IsStatic))
                {
                    if (method.Parameters.Length == 0)
                    {
                        continue;
                    }

                    var groupName    = type.FullName.Substring(type.FullName.LastIndexOf('.') + 1).Replace("+", string.Empty);
                    var docGroupName = type.Name == "X64" ? type.DeclaringType.Name : type.Name;

                    var xmlDocStr = method.GetDocumentationCommentXml();
                    if (string.IsNullOrEmpty(xmlDocStr))
                    {
                        continue;
                    }

                    var xmlDoc   = XElement.Parse($"<root>{xmlDocStr.Trim()}</root>");
                    var elements = xmlDoc.Elements().First();

                    var csharpSummary = GetCleanedString(elements);

                    var summaryTrimmed = csharpSummary.Replace("unsigned", string.Empty);
                    var match          = regexName.Match(summaryTrimmed);
                    if (!match.Success)
                    {
                        continue;
                    }

                    var rawIntrinsicName = match.Groups[1].Value;
                    var intrinsicName    = rawIntrinsicName.TrimStart('_');

                    var desc = new KalkIntrinsicToGenerate
                    {
                        Name         = intrinsicName,
                        Class        = groupName,
                        MethodSymbol = method,
                        IsFunc       = true,
                    };

                    bool hasInteldoc = false;
                    if (nameToDoc.TryGetValue(rawIntrinsicName, out var elementIntelDoc))
                    {
                        hasInteldoc      = true;
                        desc.Description = GetCleanedString(elementIntelDoc.Descendants("description").FirstOrDefault());

                        foreach (var parameter in elementIntelDoc.Descendants("parameter"))
                        {
                            desc.Params.Add(new KalkParamDescriptor(parameter.Attribute("varname").Value, parameter.Attribute("type").Value));
                        }

                        desc.Description = desc.Description.Replace("[round_note]", string.Empty).Trim();
                        desc.Description = desc.Description + "\n\n" + csharpSummary;
                    }
                    else
                    {
                        desc.Description = csharpSummary;
                    }

                    // Patching special methods
                    switch (desc.Name)
                    {
                    case "mm_prefetch":
                        if (method.Name.EndsWith("0"))
                        {
                            desc.Name += "0";
                        }
                        else if (method.Name.EndsWith("1"))
                        {
                            desc.Name += "1";
                        }
                        else if (method.Name.EndsWith("2"))
                        {
                            desc.Name += "2";
                        }
                        else if (method.Name.EndsWith("Temporal"))
                        {
                            desc.Name += "nta";
                        }
                        else
                        {
                            goto default;
                        }
                        break;

                    case "mm_round_sd":
                    case "mm_round_ss":
                    case "mm_round_pd":
                    case "mm_round_ps":
                    case "mm256_round_pd":
                    case "mm256_round_ps":
                        Debug.Assert(method.Name.StartsWith("Round"));
                        var postfix = method.Name.Substring("Round".Length);
                        Debug.Assert(hasInteldoc);
                        if (desc.Name != "mm_round_ps" && desc.Name != "mm256_round_ps" && (desc.Params.Count >= 2 && method.Parameters.Length == 1))
                        {
                            desc.Name += "1";
                        }

                        if (!postfix.StartsWith("CurrentDirection"))
                        {
                            postfix   = StandardMemberRenamer.Rename(postfix);
                            desc.Name = $"{desc.Name}_{postfix}";
                        }

                        // Remove rounding from doc
                        var index = desc.Params.FindIndex(x => x.Name == "rounding");
                        if (index >= 0)
                        {
                            desc.Params.RemoveAt(index);
                        }
                        break;

                    case "mm_rcp_ss":
                    case "mm_rsqrt_ss":
                    case "mm_sqrt_ss":
                        if (method.Parameters.Length == 1)
                        {
                            desc.Name += "1";
                        }
                        break;

                    case "mm_sqrt_sd":
                    case "mm_ceil_sd":
                    case "mm_floor_sd":
                        if (method.Parameters.Length == 1)
                        {
                            desc.Name += "1";
                        }
                        break;

                    default:
                        if (hasInteldoc)
                        {
                            if (method.Parameters.Length != desc.Params.Count)
                            {
                                Console.WriteLine($"Parameters not matching for {method.ToDisplayString()}. Expecting: {desc.Params.Count} but got {method.Parameters.Length}  ");
                            }
                        }
                        break;
                    }

                    desc.CSharpName = desc.Name;
                    desc.Names.Add(desc.Name);
                    desc.RealReturnType = method.ReturnType.ToDisplayString();
                    if (desc.RealReturnType == "bool")
                    {
                        desc.RealReturnType = "KalkBool";
                    }
                    desc.ReturnType = method.ReturnType is INamedTypeSymbol retType && retType.IsGenericType ? "object" : desc.RealReturnType;
                    desc.GenericCompatibleRealReturnType = method.ReturnType is IPointerTypeSymbol ? "IntPtr" : desc.RealReturnType;

                    (desc.BaseNativeReturnType, desc.NativeReturnType) = GetBaseTypeAndType(method.ReturnType);

                    desc.HasPointerArguments = false;

                    for (int i = 0; i < method.Parameters.Length; i++)
                    {
                        var intrinsicParameter = new KalkIntrinsicParameterToGenerate();
                        var parameter          = method.Parameters[i];
                        intrinsicParameter.Name = i < desc.Params.Count ? desc.Params[i].Name : parameter.Name;

                        var parameterType = method.Parameters[i].Type;
                        intrinsicParameter.Type     = parameterType is INamedTypeSymbol paramType && paramType.IsGenericType || parameterType is IPointerTypeSymbol ? "object" : parameter.Type.ToDisplayString();
                        intrinsicParameter.RealType = parameter.Type.ToDisplayString();
                        intrinsicParameter.GenericCompatibleRealType = parameterType is IPointerTypeSymbol ? "IntPtr" : intrinsicParameter.RealType;
                        if (parameterType is IPointerTypeSymbol)
                        {
                            desc.HasPointerArguments = true;
                        }

                        (intrinsicParameter.BaseNativeType, intrinsicParameter.NativeType) = GetBaseTypeAndType(parameter.Type);
                        desc.Parameters.Add(intrinsicParameter);
                    }

                    // public object mm_add_ps(object left, object right) => ProcessArgs<float, Vector128<float>, float, Vector128<float>, float, Vector128<float>>(left, right, Sse.Add);
                    var methodDeclaration = new StringBuilder();
                    methodDeclaration.Append($"public {desc.ReturnType} {desc.Name}(");
                    for (int i = 0; i < desc.Parameters.Count; i++)
                    {
                        var parameter = desc.Parameters[i];
                        if (i > 0)
                        {
                            methodDeclaration.Append(", ");
                        }
                        methodDeclaration.Append($"{parameter.Type} {parameter.Name}");
                    }

                    bool isAction = method.ReturnType.ToDisplayString() == "void";
                    desc.IsAction = isAction;
                    desc.IsFunc   = !desc.IsAction;

                    methodDeclaration.Append(isAction ? $") => ProcessAction<" : $") => ({desc.ReturnType})ProcessFunc<");

                    var castBuilder = new StringBuilder(isAction ? "Action<" : "Func<");
                    for (int i = 0; i < desc.Parameters.Count; i++)
                    {
                        var parameter = desc.Parameters[i];
                        if (i > 0)
                        {
                            methodDeclaration.Append(", ");
                            castBuilder.Append(", ");
                        }
                        methodDeclaration.Append($"{parameter.BaseNativeType}, {parameter.NativeType}");
                        castBuilder.Append($"{parameter.Type}");
                    }

                    if (!isAction)
                    {
                        if (desc.Parameters.Count > 0)
                        {
                            methodDeclaration.Append(", ");
                            castBuilder.Append(", ");
                        }

                        methodDeclaration.Append($"{desc.BaseNativeReturnType}, {desc.NativeReturnType}");
                        castBuilder.Append($"{desc.ReturnType}");
                    }
                    methodDeclaration.Append($">(");
                    castBuilder.Append($">");

                    // Gets any memory alignment
                    RequiredAlignments.TryGetValue(intrinsicName, out int memAlign);

                    for (int i = 0; i < desc.Parameters.Count; i++)
                    {
                        var parameter = desc.Parameters[i];
                        methodDeclaration.Append($"{parameter.Name}, ");
                        if (memAlign > 0 && parameter.Name == "mem_addr")
                        {
                            methodDeclaration.Append($"{memAlign}, ");
                        }
                    }

                    var finalSSEMethod = $"{method.ContainingType.ToDisplayString()}.{method.Name}";
                    var sseMethodName  = desc.HasPointerArguments ? $"{groupName}_{method.Name}{count}" : finalSSEMethod;
                    methodDeclaration.Append(sseMethodName);
                    methodDeclaration.Append(");");
                    desc.Cast = $"({castBuilder})";
                    desc.MethodDeclaration = methodDeclaration.ToString();

                    if (desc.HasPointerArguments)
                    {
                        methodDeclaration.Clear();

                        castBuilder.Clear();
                        castBuilder.Append(isAction ? "Action<" : "Func<");
                        for (int i = 0; i < desc.Parameters.Count; i++)
                        {
                            var parameter = desc.Parameters[i];
                            if (i > 0)
                            {
                                castBuilder.Append(", ");
                            }
                            castBuilder.Append($"{parameter.GenericCompatibleRealType}");
                        }

                        if (!isAction)
                        {
                            castBuilder.Append($", {desc.GenericCompatibleRealReturnType}");
                        }
                        castBuilder.Append(">");

                        methodDeclaration.Append($"private unsafe readonly static {castBuilder} {sseMethodName} = new {castBuilder}((");
                        for (int i = 0; i < desc.Parameters.Count; i++)
                        {
                            if (i > 0)
                            {
                                methodDeclaration.Append(", ");
                            }
                            methodDeclaration.Append($"arg{i}");
                        }
                        methodDeclaration.Append($") => {finalSSEMethod}(");
                        for (int i = 0; i < desc.Parameters.Count; i++)
                        {
                            if (i > 0)
                            {
                                methodDeclaration.Append(", ");
                            }
                            var parameter = desc.Parameters[i];
                            methodDeclaration.Append($"({parameter.RealType})arg{i}");
                        }
                        methodDeclaration.Append("));");
                        desc.IndirectMethodDeclaration = methodDeclaration.ToString();
                    }

                    desc.Category = $"Vector Hardware Intrinsics / {docGroupName.ToUpperInvariant()}";

                    generatedIntrinsics.TryGetValue(desc.Name, out var existingDesc);

                    // TODO: handle line for comments
                    desc.Description = desc.Description.Trim().Replace("\r\n", "\n");
                    if (existingDesc == null || desc.Parameters[0].BaseNativeType == "float")
                    {
                        generatedIntrinsics[desc.Name] = desc;
                    }

                    desc.IsSupported = true;

                    count++;
                }
            }

            Console.WriteLine($"{generatedIntrinsics.Count} intrinsics");

            var intrinsics = generatedIntrinsics.Values.Where(x => x.IsSupported).ToList();

            var intrinsicsPerClass = intrinsics.GroupBy(x => x.Class).ToDictionary(x => x.Key, y => y.OrderBy(x => x.Name).ToList());

            var modules = new List <KalkModuleToGenerate>();

            foreach (var keyPair in intrinsicsPerClass.OrderBy(x => x.Key))
            {
                var module = new KalkModuleToGenerate
                {
                    Namespace = "Kalk.Core.Modules.HardwareIntrinsics",
                    ClassName = $"{keyPair.Key}IntrinsicsModule",
                    Category  = keyPair.Value.First().Category
                };
                module.Members.AddRange(keyPair.Value);
                modules.Add(module);
            }

            return(modules);
        }
예제 #3
0
        static async Task Main(string[] args)
        {
            var x = typeof(System.Composition.CompositionContext).Name;

            var rootFolder  = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, @"../../../../.."));
            var siteFolder  = Path.Combine(rootFolder, "site");
            var srcFolder   = Path.Combine(rootFolder, "src");
            var testsFolder = Path.Combine(srcFolder, "Kalk.Tests");

            var pathToSolution = Path.Combine(srcFolder, @"Kalk.Core", "Kalk.Core.csproj");

            var broResult = CSharpCompilationCapture.Build(pathToSolution);
            var solution  = broResult.Workspace.CurrentSolution;
            var project   = solution.Projects.First(x => x.Name == "Kalk.Core");

            // Make sure that doc will be parsed
            project = project.WithParseOptions(project.ParseOptions.WithDocumentationMode(DocumentationMode.Parse));

            // Compile the project
            var compilation = await project.GetCompilationAsync();

            var errors = compilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList();

            if (errors.Count > 0)
            {
                Console.WriteLine("Compilation errors:");
                foreach (var error in errors)
                {
                    Console.WriteLine(error);
                }

                Console.WriteLine("Error, Exiting.");
                Environment.Exit(1);
                return;
            }

            //var kalkEngine = compilation.GetTypeByMetadataName("Kalk.Core.KalkEngine");

            var mapNameToModule = new Dictionary <string, KalkModuleToGenerate>();

            void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData moduleAttribute, out KalkModuleToGenerate moduleToGenerate)
            {
                if (!mapNameToModule.TryGetValue(className, out moduleToGenerate))
                {
                    moduleToGenerate = new KalkModuleToGenerate()
                    {
                        Namespace = typeSymbol.ContainingNamespace.ToDisplayString(),
                        ClassName = className,
                    };
                    mapNameToModule.Add(className, moduleToGenerate);

                    if (moduleAttribute != null)
                    {
                        moduleToGenerate.Name = moduleAttribute.ConstructorArguments[0].Value.ToString();
                        moduleToGenerate.Names.Add(moduleToGenerate.Name);
                        moduleToGenerate.Category = "Modules (e.g `import Files`)";
                    }
                    else
                    {
                        moduleToGenerate.Name      = className.Replace("Module", "");
                        moduleToGenerate.IsBuiltin = true;
                    }

                    ExtractDocumentation(typeSymbol, moduleToGenerate);
                }
            }

            foreach (var type in compilation.GetSymbolsWithName(x => true, SymbolFilter.Type))
            {
                var typeSymbol = type as ITypeSymbol;
                if (typeSymbol == null)
                {
                    continue;
                }

                var moduleAttribute = typeSymbol.GetAttributes().FirstOrDefault(x => x.AttributeClass.Name == "KalkExportModuleAttribute");
                KalkModuleToGenerate moduleToGenerate = null;
                if (moduleAttribute != null)
                {
                    GetOrCreateModule(typeSymbol, typeSymbol.Name, moduleAttribute, out moduleToGenerate);
                }

                foreach (var member in typeSymbol.GetMembers())
                {
                    var attr = member.GetAttributes().FirstOrDefault(x => x.AttributeClass.Name == "KalkExportAttribute");
                    if (attr == null)
                    {
                        continue;
                    }

                    var name     = attr.ConstructorArguments[0].Value.ToString();
                    var category = attr.ConstructorArguments[1].Value.ToString();

                    var containingType = member.ContainingSymbol;
                    var className      = containingType.Name;

                    // In case the module is built-in, we still generate a module for it
                    if (moduleToGenerate == null)
                    {
                        GetOrCreateModule(typeSymbol, className, moduleAttribute, out moduleToGenerate);
                    }

                    var method = member as IMethodSymbol;
                    var desc   = new KalkMemberToGenerate()
                    {
                        Name      = name,
                        XmlId     = member.GetDocumentationCommentId(),
                        Category  = category,
                        IsCommand = method != null && method.ReturnsVoid,
                        Module    = moduleToGenerate,
                    };
                    desc.Names.Add(name);

                    if (method != null)
                    {
                        desc.CSharpName = method.Name;

                        var builder = new StringBuilder();
                        desc.IsAction = method.ReturnsVoid;
                        desc.IsFunc   = !desc.IsAction;
                        builder.Append(desc.IsAction ? "Action" : "Func");

                        if (method.Parameters.Length > 0 || desc.IsFunc)
                        {
                            builder.Append("<");
                        }

                        for (var i = 0; i < method.Parameters.Length; i++)
                        {
                            var parameter = method.Parameters[i];
                            if (i > 0)
                            {
                                builder.Append(", ");
                            }
                            builder.Append(GetTypeName(parameter.Type));
                        }

                        if (desc.IsFunc)
                        {
                            if (method.Parameters.Length > 0)
                            {
                                builder.Append(", ");
                            }
                            builder.Append(GetTypeName(method.ReturnType));
                        }

                        if (method.Parameters.Length > 0 || desc.IsFunc)
                        {
                            builder.Append(">");
                        }

                        desc.Cast = $"({builder.ToString()})";
                    }
                    else if (member is IPropertySymbol || member is IFieldSymbol)
                    {
                        desc.CSharpName = member.Name;
                        desc.IsConst    = true;
                    }

                    moduleToGenerate.Members.Add(desc);
                    ExtractDocumentation(member, desc);
                }
            }

            var modules = mapNameToModule.Values.OrderBy(x => x.ClassName).ToList();

            var templateStr = @"//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Date: {{ date.now }}
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;

{{~ func GenerateDocDescriptor(item) ~}}
            {
                var descriptor = {{ if item.IsModule }}Descriptor{{ else }}Descriptors[""{{ item.Name }}""]{{ end }};
                descriptor.Category = ""{{ item.Category }}"";
                descriptor.Description = @""{{ item.Description | string.replace '""' '""""' }}"";
                descriptor.IsCommand = {{ item.IsCommand }};
            {{~ for param in item.Params ~}}
                descriptor.Params.Add(new KalkParamDescriptor(""{{ param.Name }}"", @""{{ param.Description | string.replace '""' '""""' }}"")  { IsOptional = {{ param.IsOptional }} });
            {{~ end ~}}
                {{~ if item.Returns ~}}
                descriptor.Returns = @""{{ item.Returns | string.replace '""' '""""' }}"";
                {{~ end ~}}
                {{~ if item.Remarks ~}}
                descriptor.Remarks = @""{{ item.Remarks | string.replace '""' '""""' }}"";
                {{~ end ~}}
                {{~ if item.Example ~}}
                descriptor.Example = @""{{ item.Example | string.replace '""' '""""' }}"";
                {{~ end ~}}
            }
{{~ end ~}}
{{~ for module in modules ~}}
namespace {{ module.Namespace }}
{
    public partial class {{ module.ClassName }}
    {
    {{~ if module.Name != 'All' ~}}
        {{~ if module.ClassName == 'KalkEngine' ~}}
        protected void RegisterFunctionsAuto()
        {{~ else ~}}
        protected override void RegisterFunctionsAuto()
        {{~ end ~}}
        {
            {{~ for member in module.Members ~}}
                {{~ if member.IsConst ~}}
            RegisterConstant(""{{ member.Name }}"", {{ member.CSharpName }});
                {{~ else if member.IsFunc ~}}
            RegisterFunction(""{{ member.Name }}"", {{member.Cast}}{{ member.CSharpName }});
                {{~ else if member.IsAction ~}}
            RegisterAction(""{{ member.Name }}"", {{member.Cast}}{{ member.CSharpName }});
                {{~ end ~}}
            {{~ end ~}}
            RegisterDocumentationAuto();
        }

    {{~ end ~}}
        private void RegisterDocumentationAuto()
        {
            {{~ 
            if !module.IsBuiltin 
                GenerateDocDescriptor module
            end
            for item in module.Members
                GenerateDocDescriptor item
            end
            ~}}
        }        
    }
}
{{~ end ~}}
";
            var template    = Template.Parse(templateStr);

            var result = template.Render(new { modules = modules }, x => x.Name);

            File.WriteAllText(Path.Combine(srcFolder, "Kalk.Core/KalkEngine.generated.cs"), result);


            var testsTemplateStr = @"//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Date: {{ date.now }}
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using NUnit.Framework;

namespace Kalk.Tests
{
{{~ imported = {}; for module in modules; if !(imported[module.Namespace]); imported[module.Namespace] = true; ~}}
    using {{ module.Namespace }};
{{~ end; end ~}}
{{~ for module in modules ~}}

    public partial class {{ module.ClassName }}Tests : KalkTestBase
    {
        {{~ for member in module.Members ~}}
            {{~ if (array.size member.Tests) > 0 ~}}
                {{~ for test in member.Tests ~}}
        /// <summary>
        /// Test for <see cref=""{{ member.XmlId }}""/> or `{{ member.Name }}`.
        /// </summary>
        [TestCase(@""{{ test.Item1 | string.replace '""' '""""' }}"", @""{{ test.Item2 | string.replace '""' '""""' }}"", Category = ""{{ member.Category }}"")]
                {{~ end ~}}
        {{~ if module.IsBuiltin ~}}
        public static void Test_{{ member.Name }}(string input, string output) => AssertScript(input, output);
        {{~ else ~}}
        public static void Test_{{ member.Name }}(string input, string output) => AssertScript(input, output, ""{{module.Name}}"");
        {{~ end ~}}

            {{~ end ~}}
        {{~ end ~}}
    }
{{~ end ~}}
}
";
            var templateTests    = Template.Parse(testsTemplateStr);

            result = templateTests.Render(new { modules = modules }, x => x.Name);
            File.WriteAllText(Path.Combine(testsFolder, "KalkTests.generated.cs"), result);

            // Prism templates
            var prismTemplateStr = @"Prism.languages.kalk.function = /\b(?:{{ functions | array.join '|' }})\b/;
";
            var prismTemplate    = Template.Parse(prismTemplateStr);

            var allFunctionNames = modules.SelectMany(x => x.Members.Select(y => y.Name)).ToList();

            // special values
            allFunctionNames.AddRange(new [] { "null", "true", "false" });
            // modules
            allFunctionNames.AddRange(new[] { "All", "Csv", "Currencies", "Files", "HardwareIntrinsics", "StandardUnits", "Strings", "Web" });

            allFunctionNames = allFunctionNames.OrderByDescending(x => x.Length).ThenBy(x => x).ToList();
            result           = prismTemplate.Render(new { functions = allFunctionNames });
            File.WriteAllText(Path.Combine(siteFolder, ".lunet", "js", "prism-kalk.generated.js"), result);

            // Generate module site documentation
            foreach (var module in modules)
            {
                GenerateModuleSiteDocumentation(module, siteFolder);
            }

            // Log any errors if a member doesn't have any doc or tests
            int functionWithMissingDoc   = 0;
            int functionWithMissingTests = 0;

            foreach (var module in modules)
            {
                foreach (var member in module.Members)
                {
                    var hasNoDesc  = string.IsNullOrEmpty(member.Description);
                    var hasNoTests = member.Tests.Count == 0;
                    if ((hasNoDesc || hasNoTests) && !module.ClassName.Contains("Intrinsics"))
                    {
                        // We don't log for all the matrix constructors, as they are tested separately.
                        if (module.ClassName != "TypesModule" || !member.CSharpName.StartsWith("Create"))
                        {
                            if (hasNoDesc)
                            {
                                ++functionWithMissingDoc;
                            }
                            if (hasNoTests)
                            {
                                ++functionWithMissingTests;
                            }
                            Console.WriteLine($"The member {member.Name} => {module.ClassName}.{member.CSharpName} doesn't have {(hasNoTests ? "any tests" + (hasNoDesc ? " and" : "") : "")} {(hasNoDesc ? "any docs" : "")}");
                        }
                    }
                }
            }

            Console.WriteLine($"{modules.Count} modules generated.");
            Console.WriteLine($"{modules.SelectMany(x => x.Members).Count()} functions generated.");
            Console.WriteLine($"{modules.SelectMany(x => x.Members).SelectMany(y => y.Tests).Count()} tests generated.");
            Console.WriteLine($"{functionWithMissingDoc} functions with missing doc.");
            Console.WriteLine($"{functionWithMissingTests} functions with missing tests.");
        }