using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace YesNt.Interpreter.Generator; [Generator] public sealed class StatementRegistryGenerator : IIncrementalGenerator { private const string StatementAttributeName = "YesNt.Interpreter.Attributes.StatementAttribute"; private const string StaticStatementAttributeName = "YesNt.Interpreter.Attributes.StaticStatementAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterSourceOutput( context.CompilationProvider, Execute); } private static void Execute(SourceProductionContext context, Compilation compilation) { List statementMethods = []; List staticStatementMethods = []; CollectMethods(compilation.Assembly.GlobalNamespace, statementMethods, staticStatementMethods); INamedTypeSymbol? statementAttrSymbol = compilation.GetTypeByMetadataName(StatementAttributeName); INamedTypeSymbol? staticStatementAttrSymbol = compilation.GetTypeByMetadataName(StaticStatementAttributeName); string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol, compilation); context.AddSource("GeneratedStatementInformation.g.cs", infoSource); string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods); context.AddSource("GeneratedStatementRegistry.g.cs", registrySource); } private static void CollectMethods( INamespaceSymbol namespaceSymbol, List statementMethods, List staticStatementMethods) { foreach (INamespaceSymbol childNamespace in namespaceSymbol.GetNamespaceMembers()) { CollectMethods(childNamespace, statementMethods, staticStatementMethods); } foreach (INamedTypeSymbol type in namespaceSymbol.GetTypeMembers()) { CollectMethods(type, statementMethods, staticStatementMethods); } } private static void CollectMethods( INamedTypeSymbol typeSymbol, List statementMethods, List staticStatementMethods) { foreach (ISymbol member in typeSymbol.GetMembers()) { if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) { foreach (AttributeData attribute in method.GetAttributes()) { string? attributeName = attribute.AttributeClass?.ToDisplayString(); if (attributeName == StatementAttributeName) { statementMethods.Add(new MethodRegistration(typeSymbol, method, attribute)); } else if (attributeName == StaticStatementAttributeName) { staticStatementMethods.Add(new MethodRegistration(typeSymbol, method, attribute)); } } } } foreach (INamedTypeSymbol nestedType in typeSymbol.GetTypeMembers()) { CollectMethods(nestedType, statementMethods, staticStatementMethods); } } // ------------------------------------------------------------------------- // Information class generation // ------------------------------------------------------------------------- private static string GenerateInformationClassesSource( INamedTypeSymbol? statementAttrSymbol, INamedTypeSymbol? staticStatementAttrSymbol, Compilation compilation) { StringBuilder sb = new StringBuilder(); _ = sb.AppendLine("// "); _ = sb.AppendLine("#nullable enable"); _ = sb.AppendLine(); _ = sb.AppendLine("namespace YesNt.Interpreter.Runtime;"); _ = sb.AppendLine(); if (statementAttrSymbol is not null) { EmitInformationClass(sb, statementAttrSymbol, "StatementInformation", compilation); } if (staticStatementAttrSymbol is not null) { EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation", compilation); } return sb.ToString(); } private static void EmitInformationClass( StringBuilder sb, INamedTypeSymbol attributeSymbol, string className, Compilation compilation) { List ctors = attributeSymbol.Constructors .Where(c => !c.IsImplicitlyDeclared) .OrderByDescending(c => c.Parameters.Length) .ToList(); // All parameters across all ctors that are covered by a ctor (get-only properties). HashSet ctorParamNames = new HashSet( ctors.SelectMany(c => c.Parameters).Select(p => p.Name), StringComparer.OrdinalIgnoreCase); // Settable properties NOT covered by any ctor parameter. List settableProps = attributeSymbol .GetMembers() .OfType() .Where(p => !p.IsStatic && p.DeclaredAccessibility == Accessibility.Public && !ctorParamNames.Contains(p.Name)) .ToList(); // All properties ever assigned by any ctor, deduplicated by name. // We need a get-only property for each of them. var ctorProps = ctors .SelectMany(c => c.Parameters) .Select(p => { string propName = ResolvePropertyName(attributeSymbol, p); IPropertySymbol? propSymbol = attributeSymbol .GetMembers() .OfType() .FirstOrDefault(sym => string.Equals(sym.Name, propName, StringComparison.OrdinalIgnoreCase)); return (PropName: propName, PropType: p.Type, PropSymbol: propSymbol); }) .GroupBy(x => x.PropName, StringComparer.OrdinalIgnoreCase) .Select(g => g.First()) .ToList(); // ---- class declaration ---- _ = sb.Append(GetFormattedComment(attributeSymbol, attributeSymbol, className, $"Information about the {className} class.", "")); _ = sb.AppendLine($"public sealed class {className}"); _ = sb.AppendLine("{"); // ---- one constructor per attribute constructor ---- foreach (IMethodSymbol ctor in ctors) { if (ctor.Parameters.Length == 0) { _ = sb.Append(GetFormattedComment(ctor, attributeSymbol, className, $"Initializes a new instance of the class.", " ")); _ = sb.AppendLine($" public {className}() {{ }}"); _ = sb.AppendLine(); continue; } string ctorParams = string.Join(", ", ctor.Parameters.Select(p => $"{GlobalType(p.Type)} {p.Name}")); _ = sb.Append(GetFormattedComment(ctor, attributeSymbol, className, $"Initializes a new instance of the class.", " ")); _ = sb.AppendLine($" public {className}({ctorParams})"); _ = sb.AppendLine(" {"); foreach (IParameterSymbol p in ctor.Parameters) { string propName = ResolvePropertyName(attributeSymbol, p); _ = sb.AppendLine($" {propName} = {p.Name};"); } _ = sb.AppendLine(" }"); _ = sb.AppendLine(); } // ---- get-only properties sourced from ctor parameters ---- foreach (var ctorProp in ctorProps) { if (ctorProp.PropSymbol is not null) { _ = sb.Append(GetFormattedComment(ctorProp.PropSymbol, attributeSymbol, className, $"Gets the {ctorProp.PropName} property.", " ")); } else { _ = sb.AppendLine(" /// "); _ = sb.AppendLine($" /// Gets the {ctorProp.PropName} property."); _ = sb.AppendLine(" /// "); } _ = sb.AppendLine($" public {GlobalType(ctorProp.PropType)} {ctorProp.PropName} {{ get; }}"); } // ---- settable properties (named-argument style) ---- foreach (IPropertySymbol prop in settableProps) { string defaultClause = GetDefaultClause(prop, compilation); _ = sb.Append(GetFormattedComment(prop, attributeSymbol, className, $"Gets or sets the {prop.Name} property.", " ")); _ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}"); } _ = sb.AppendLine("}"); _ = sb.AppendLine(); } private static string GetFormattedComment( ISymbol symbol, INamedTypeSymbol attributeSymbol, string className, string fallbackSummary, string indent = " ") { string? xml = symbol.GetDocumentationCommentXml(); if (!string.IsNullOrEmpty(xml)) { xml = xml!.Replace(attributeSymbol.Name, className); } string formatted = FormatXmlComment(xml, indent); if (!string.IsNullOrWhiteSpace(formatted)) { return formatted; } // Fallback var sb = new StringBuilder(); _ = sb.AppendLine($"{indent}/// "); _ = sb.AppendLine($"{indent}/// {fallbackSummary}"); _ = sb.AppendLine($"{indent}/// "); return sb.ToString(); } private static string FormatXmlComment(string? xml, string indent) { if (string.IsNullOrWhiteSpace(xml)) { return string.Empty; } try { var element = System.Xml.Linq.XElement.Parse(xml!); var sb = new StringBuilder(); foreach (var child in element.Elements()) { string nodeXml = child.ToString(); string[] lines = nodeXml.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); foreach (string line in lines) { string trimmedLine = line.Trim(); if (trimmedLine.Length > 0) { _ = sb.AppendLine($"{indent}/// {trimmedLine}"); } } } return sb.ToString(); } catch { return string.Empty; } } // Finds the attribute property that corresponds to a constructor parameter. private static string ResolvePropertyName(INamedTypeSymbol attributeSymbol, IParameterSymbol parameter) { IPropertySymbol? match = attributeSymbol .GetMembers() .OfType() .FirstOrDefault(p => string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); return match?.Name ?? ToPascalCase(parameter.Name); } private static string GlobalType(ITypeSymbol type) { // Primitive aliases don't have a global:: form — emit the C# keyword instead. string keyword = type.SpecialType switch { SpecialType.System_String => "string", SpecialType.System_Boolean => "bool", SpecialType.System_Byte => "byte", SpecialType.System_SByte => "sbyte", SpecialType.System_Int16 => "short", SpecialType.System_UInt16 => "ushort", SpecialType.System_Int32 => "int", SpecialType.System_UInt32 => "uint", SpecialType.System_Int64 => "long", SpecialType.System_UInt64 => "ulong", SpecialType.System_Single => "float", SpecialType.System_Double => "double", SpecialType.System_Decimal => "decimal", SpecialType.System_Char => "char", SpecialType.System_Object => "object", _ => "" }; if (keyword != "") { // Preserve nullability annotation (e.g. string?) return type.NullableAnnotation == NullableAnnotation.Annotated ? keyword + "?" : keyword; } return $"global::{type.ToDisplayString()}"; } private static string ToPascalCase(string name) { return string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name.Substring(1); } private static string GetDefaultClause( IPropertySymbol prop, Compilation compilation) { foreach (SyntaxReference syntaxRef in prop.DeclaringSyntaxReferences) { if (syntaxRef.GetSyntax() is not Microsoft.CodeAnalysis.CSharp.Syntax.PropertyDeclarationSyntax syntax) { continue; } if (syntax.Initializer?.Value is null) { continue; } SemanticModel model = compilation.GetSemanticModel(syntax.SyntaxTree); ExpressionSyntax expr = syntax.Initializer.Value; // 1. Try constant evaluation first (SAFE) Optional constant = model.GetConstantValue(expr); if (constant.HasValue) { return $" = {ToLiteral(constant.Value!, prop.Type)};"; } // 2. Try symbol resolution (enum fields etc.) ISymbol? symbolInfo = model.GetSymbolInfo(expr).Symbol; if (symbolInfo is IFieldSymbol field) { string typeName = GlobalType(field.ContainingType); return $" = {typeName}.{field.Name};"; } // 3. fallback: raw expression (last resort) return $" = {expr};"; } // fallback defaults return prop.Type.IsReferenceType || prop.NullableAnnotation == NullableAnnotation.Annotated ? " = null!;" : prop.Type.SpecialType switch { SpecialType.System_Boolean => " = false;", SpecialType.System_Int32 => " = 0;", SpecialType.System_String => " = \"\";", _ => "" }; } // ------------------------------------------------------------------------- // Registry source generation (unchanged from original) // ------------------------------------------------------------------------- private static string GenerateRegistrySource( List statementMethods, List staticStatementMethods) { StringBuilder sb = new StringBuilder(); _ = sb.AppendLine("// "); _ = sb.AppendLine("#nullable enable"); _ = sb.AppendLine("using System;"); _ = sb.AppendLine("using System.Collections.Generic;"); _ = sb.AppendLine(); _ = sb.AppendLine("namespace YesNt.Interpreter.Runtime;"); _ = sb.AppendLine(); _ = sb.AppendLine("internal static class GeneratedStatementRegistry"); _ = sb.AppendLine("{"); _ = sb.AppendLine(" internal static void Register("); _ = sb.AppendLine(" RuntimeInformation runtimeInfo,"); _ = sb.AppendLine(" out Dictionary> statements,"); _ = sb.AppendLine(" out List> staticStatements)"); _ = sb.AppendLine(" {"); List allTypes = statementMethods .Concat(staticStatementMethods) .Select(x => x.ContainingType) .GroupBy(x => x, SymbolEqualityComparer.Default) .Select(g => g.First()) .OrderBy(x => x.ToDisplayString()) .ToList(); Dictionary instanceNames = new Dictionary(SymbolEqualityComparer.Default); int index = 0; foreach (INamedTypeSymbol type in allTypes) { string instanceName = $"instance{index++}"; instanceNames[type] = instanceName; _ = sb.AppendLine($" var {instanceName} = new global::{type.ToDisplayString()}();"); _ = sb.AppendLine($" {instanceName}.RuntimeInfo = runtimeInfo;"); } _ = sb.AppendLine(" var statementEntries = new List>>();"); foreach (MethodRegistration method in statementMethods .OrderBy(x => x.ContainingType.ToDisplayString()) .ThenBy(x => x.Method.Name)) { string instanceName = instanceNames[method.ContainingType]; string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Runtime.StatementInformation", method.Attribute); _ = sb.AppendLine($" statementEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); } _ = sb.AppendLine(" var staticEntries = new List>();"); foreach (MethodRegistration method in staticStatementMethods .OrderBy(x => x.ContainingType.ToDisplayString()) .ThenBy(x => x.Method.Name)) { string instanceName = instanceNames[method.ContainingType]; string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Runtime.StaticStatementInformation", method.Attribute); _ = sb.AppendLine($" staticEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); } _ = sb.AppendLine(" statementEntries.Sort((a, b) =>"); _ = sb.AppendLine(" {"); _ = sb.AppendLine(" int cmp = a.Key.Priority.CompareTo(b.Key.Priority);"); _ = sb.AppendLine(" return cmp != 0 ? cmp : b.Key.Name.Length.CompareTo(a.Key.Name.Length);"); _ = sb.AppendLine(" });"); _ = sb.AppendLine(" statements = new Dictionary>();"); _ = sb.AppendLine(" foreach (var entry in statementEntries)"); _ = sb.AppendLine(" statements.Add(entry.Key, entry.Value);"); _ = sb.AppendLine(); _ = sb.AppendLine(" staticEntries.Sort((a, b) => a.Key.Priority.CompareTo(b.Key.Priority));"); _ = sb.AppendLine(" staticStatements = staticEntries;"); _ = sb.AppendLine(" }"); _ = sb.AppendLine("}"); return sb.ToString(); } private static string BuildAttributeCreation(string attributeTypeName, AttributeData attribute) { string ctorArgs = string.Join(", ", attribute.ConstructorArguments.Select(ToLiteral)); string creation = $"new {attributeTypeName}({ctorArgs})"; if (attribute.NamedArguments.Length == 0) { return creation; } string namedArgs = string.Join(", ", attribute.NamedArguments.Select(arg => $"{arg.Key} = {ToLiteral(arg.Value)}")); return $"{creation} {{ {namedArgs} }}"; } private static string ToLiteral(TypedConstant constant) { if (constant.IsNull) { return "null!"; } if (constant.Type is null) { return "null!"; } if (constant.Kind == TypedConstantKind.Enum) { string enumType = $"global::{constant.Type.ToDisplayString()}"; object value = constant.Value!; return $"({enumType}){Convert.ToInt64(value)}"; } return constant.Type.SpecialType switch { SpecialType.System_String => "\"" + EscapeString((string)constant.Value!) + "\"", SpecialType.System_Char => "'" + EscapeChar((char)constant.Value!) + "'", SpecialType.System_Boolean => (bool)constant.Value! ? "true" : "false", SpecialType.System_Int32 => ((int)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture), SpecialType.System_Int64 => ((long)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture) + "L", SpecialType.System_Single => ((float)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture) + "f", SpecialType.System_Double => ((double)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture), _ => constant.Value!.ToString() ?? "null!" }; } private static string ToLiteral(object value, ITypeSymbol type) { if (value is null) { return "null!"; } if (type.TypeKind == TypeKind.Enum) { string enumType = GlobalType(type); // value is already boxed enum OR underlying integral type long underlying = Convert.ToInt64(value); // fallback: cast return $"({enumType}){underlying}"; } return type.SpecialType switch { SpecialType.System_String => $"\"{EscapeString((string)value)}\"", SpecialType.System_Char => $"'{EscapeChar((char)value)}'", SpecialType.System_Boolean => (bool)value ? "true" : "false", SpecialType.System_Int32 => ((int)value).ToString(System.Globalization.CultureInfo.InvariantCulture), SpecialType.System_Int64 => ((long)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "L", SpecialType.System_Single => ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "f", SpecialType.System_Double => ((double)value).ToString(System.Globalization.CultureInfo.InvariantCulture), SpecialType.System_Decimal => ((decimal)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "m", _ => value.ToString() ?? "null!" }; } private static string EscapeString(string value) { return value .Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\r", "\\r") .Replace("\n", "\\n") .Replace("\t", "\\t"); } private static string EscapeChar(char value) { return value switch { '\\' => "\\\\", '\'' => "\\'", '\r' => "\\r", '\n' => "\\n", '\t' => "\\t", _ => value.ToString() }; } private sealed class MethodRegistration(INamedTypeSymbol containingType, IMethodSymbol method, AttributeData attribute) { public INamedTypeSymbol ContainingType { get; } = containingType; public IMethodSymbol Method { get; } = method; public AttributeData Attribute { get; } = attribute; } }