diff --git a/src/ChibiRuby.Serializer.SourceGenerator/ChibiRubySerializerSourceGenerator.cs b/src/ChibiRuby.Serializer.SourceGenerator/ChibiRubySerializerSourceGenerator.cs
index ea5613fc..2ab69833 100644
--- a/src/ChibiRuby.Serializer.SourceGenerator/ChibiRubySerializerSourceGenerator.cs
+++ b/src/ChibiRuby.Serializer.SourceGenerator/ChibiRubySerializerSourceGenerator.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -9,96 +9,38 @@ public class ChibiRubySerializerSourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
- var serializableProvider = context.SyntaxProvider
+ // Extract a fully value-equatable model in the transform stage. This keeps the
+ // pipeline free of Compilation/symbol/syntax references, so the incremental driver
+ // can cache per type and skip generation when nothing relevant changed.
+ var models = context.SyntaxProvider
.ForAttributeWithMetadataName(
"ChibiRuby.Serializer.MRubyObjectAttribute",
- static (node, cancellation) =>
+ static (node, _) =>
node is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax,
- static (context, cancellation) => context);
- // .Combine(context.CompilationProvider);
- //.WithComparer(Comparer.Instance)
+ static (context, cancellation) => MRubyObjectModel.Create(context, cancellation))
+ .WithTrackingName("MRubyObjectModels");
- context.RegisterSourceOutput(
- context.CompilationProvider.Combine(serializableProvider.Collect()),
- (productionContext, t) =>
- {
- var (compilation, list) = t;
- var references = ReferenceSymbols.Create(compilation);
- if (references is null)
- {
- return;
- }
-
- var stringBuilder = new StringBuilder();
-
- foreach (var x in list)
- {
- var typeMeta = new MRubyObjectTypeMeta(
- (TypeDeclarationSyntax)x.TargetNode,
- (INamedTypeSymbol)x.TargetSymbol,
- x.Attributes.First(),
- references);
-
- if (TryEmitMRubyObjectType(typeMeta, stringBuilder, references, productionContext))
- {
- var fullType = typeMeta.Symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
- .Replace("global::", "")
- .Replace("<", "_")
- .Replace(">", "_");
-
- productionContext.AddSource($"{fullType}.g.cs", stringBuilder.ToString());
- }
- stringBuilder.Clear();
- }
- });
- }
-
- static bool TryEmitMRubyObjectType(
- MRubyObjectTypeMeta typeMeta,
- StringBuilder stringBuilder,
- ReferenceSymbols references,
- in SourceProductionContext context)
- {
- try
+ context.RegisterSourceOutput(models, static (productionContext, model) =>
{
- var error = false;
-
- // verify is partial
- if (!typeMeta.IsPartial())
- {
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.MustBePartial,
- typeMeta.Syntax.Identifier.GetLocation(),
- typeMeta.Symbol.Name));
- error = true;
- }
-
- // nested is not allowed
- if (typeMeta.IsNested())
+ foreach (var diagnostic in model.Diagnostics)
{
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.NestedNotAllow,
- typeMeta.Syntax.Identifier.GetLocation(),
- typeMeta.Symbol.Name));
- error = true;
+ productionContext.ReportDiagnostic(diagnostic.ToDiagnostic());
}
- // verify abstract/interface
- if (typeMeta.Symbol.IsAbstract)
+ if (model.HasError)
{
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.AbstractNotAllow,
- typeMeta.Syntax.Identifier.GetLocation(),
- typeMeta.TypeName));
- error = true;
+ return;
}
- if (error)
- {
- return false;
- }
+ var stringBuilder = new StringBuilder();
+ Emit(model, stringBuilder);
+ productionContext.AddSource($"{model.HintName}.g.cs", stringBuilder.ToString());
+ });
+ }
- stringBuilder.AppendLine($$"""
+ static void Emit(MRubyObjectModel model, StringBuilder stringBuilder)
+ {
+ stringBuilder.AppendLine($$"""
//
#nullable enable
#pragma warning disable CS0162 // Unreachable code
@@ -115,44 +57,28 @@ static bool TryEmitMRubyObjectType(
using ChibiRuby.Serializer;
""");
- var ns = typeMeta.Symbol.ContainingNamespace;
- if (!ns.IsGlobalNamespace)
- {
- stringBuilder.AppendLine($$"""
+ if (model.Namespace is { } ns)
+ {
+ stringBuilder.AppendLine($$"""
namespace {{ns}}
{
""");
- }
-
- var typeDeclarationKeyword = (typeMeta.Symbol.IsRecord, typeMeta.Symbol.IsValueType) switch
- {
- (true, true) => "record struct",
- (true, false) => "record",
- (false, true) => "struct",
- (false, false) => "class",
- };
+ }
- stringBuilder.AppendLine($$"""
-partial {{typeDeclarationKeyword}} {{typeMeta.TypeName}}
+ stringBuilder.AppendLine($$"""
+partial {{model.TypeDeclarationKeyword}} {{model.TypeName}}
{
""");
- // EmitCCtor(typeMeta, codeWriter, in context);
- if (!TryEmitRegisterMethod(typeMeta, stringBuilder, in context))
- {
- return false;
- }
- if (!TryEmitFormatter(typeMeta, stringBuilder, references, in context))
- {
- return false;
- }
+ EmitRegisterMethod(model, stringBuilder);
+ EmitFormatter(model, stringBuilder);
+ stringBuilder.AppendLine("}");
+ if (model.Namespace is not null)
+ {
stringBuilder.AppendLine("}");
- if (!ns.IsGlobalNamespace)
- {
- stringBuilder.AppendLine("}");
- }
+ }
- stringBuilder.AppendLine($$"""
+ stringBuilder.AppendLine($$"""
#pragma warning restore CS0162 // Unreachable code
#pragma warning restore CS0219 // Variable assigned but never used
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
@@ -161,106 +87,80 @@ namespace {{ns}}
#pragma warning restore CS8604 // Possible null reference argument for parameter
#pragma warning restore CS8631 // The type cannot be used as type parameter in the generic type or method
""");
- return true;
- }
- catch (Exception ex)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.UnexpectedErrorDescriptor,
- Location.None,
- ex.ToString()));
- return false;
- }
}
- static bool TryEmitFormatter(
- MRubyObjectTypeMeta typeMeta,
- StringBuilder stringBuilder,
- ReferenceSymbols references,
- in SourceProductionContext context)
+ static void EmitRegisterMethod(MRubyObjectModel model, StringBuilder stringBuilder)
{
- var returnType = typeMeta.Symbol.IsValueType
- ? typeMeta.FullTypeName
- : $"{typeMeta.FullTypeName}?";
+ stringBuilder.AppendLine($$"""
+ [global::ChibiRuby.Serializer.Preserve]
+ public static void __RegisterMRubyValueFormatter()
+ {
+ global::ChibiRuby.Serializer.GeneratedResolver.Register(new {{model.TypeName}}GeneratedFormatter());
+ }
+
+""");
+ }
+
+ static void EmitFormatter(MRubyObjectModel model, StringBuilder stringBuilder)
+ {
+ var returnType = model.IsValueType
+ ? model.FullTypeName
+ : $"{model.FullTypeName}?";
stringBuilder.AppendLine($$"""
[global::ChibiRuby.Serializer.Preserve]
- public class {{typeMeta.TypeName}}GeneratedFormatter : IMRubyValueFormatter<{{returnType}}>
+ public class {{model.TypeName}}GeneratedFormatter : IMRubyValueFormatter<{{returnType}}>
{
""");
// Default
- foreach (var memberMeta in typeMeta.MemberMetas)
+ foreach (var member in model.Members)
{
stringBuilder.Append($$"""
- static readonly byte[] {{memberMeta.Name}}KeyUtf8 =
+ static readonly byte[] {{member.Name}}KeyUtf8 =
""");
- stringBuilder.AppendByteArrayString(memberMeta.KeyNameUtf8Bytes);
- stringBuilder.AppendLine($"; // {memberMeta.KeyName}");
+ stringBuilder.AppendByteArrayString(System.Text.Encoding.UTF8.GetBytes(member.KeyName));
+ stringBuilder.AppendLine($"; // {member.KeyName}");
}
stringBuilder.AppendLine();
- if (!TryEmitSerializeMethod(typeMeta, stringBuilder, references, in context))
- {
- return false;
- }
-
- if (!TryEmitDeserializeMethod(typeMeta, stringBuilder, references, in context))
- {
- return false;
- }
- stringBuilder.AppendLine($$"""
- }
-""");
- return true;
- }
+ EmitSerializeMethod(model, stringBuilder);
+ EmitDeserializeMethod(model, stringBuilder);
- static bool TryEmitRegisterMethod(MRubyObjectTypeMeta typeMeta, StringBuilder stringBuilder, in SourceProductionContext context)
- {
stringBuilder.AppendLine($$"""
- [global::ChibiRuby.Serializer.Preserve]
- public static void __RegisterMRubyValueFormatter()
- {
- global::ChibiRuby.Serializer.GeneratedResolver.Register(new {{typeMeta.TypeName}}GeneratedFormatter());
}
-
""");
- return true;
}
- static bool TryEmitSerializeMethod(
- MRubyObjectTypeMeta typeMeta,
- StringBuilder stringBuilder,
- ReferenceSymbols references,
- in SourceProductionContext context)
+ static void EmitSerializeMethod(MRubyObjectModel model, StringBuilder stringBuilder)
{
- var inputType = typeMeta.Symbol.IsValueType
- ? typeMeta.FullTypeName
- : $"{typeMeta.FullTypeName}?";
+ var inputType = model.IsValueType
+ ? model.FullTypeName
+ : $"{model.FullTypeName}?";
stringBuilder.AppendLine($$"""
[global::ChibiRuby.Serializer.Preserve]
public MRubyValue Serialize({{inputType}} value, MRubyState state, MRubyValueSerializerOptions options)
{
""");
- if (!typeMeta.Symbol.IsValueType)
+ if (!model.IsValueType)
{
stringBuilder.AppendLine($$"""
if (value is null) return default;
""");
}
stringBuilder.AppendLine($$"""
- var props = state.NewHash({{typeMeta.MemberMetas.Count}});
+ var props = state.NewHash({{model.Members.Count}});
""");
- foreach (var memberMeta in typeMeta.MemberMetas)
+ foreach (var member in model.Members)
{
stringBuilder.AppendLine($$"""
- var key{{memberMeta.Name}} = new MRubyValue(state.Intern({{memberMeta.Name}}KeyUtf8));
- var value{{memberMeta.Name}} = options.Resolver.GetFormatterWithVerify<{{memberMeta.FullTypeName}}>()
- .Serialize(value.{{memberMeta.Name}}, state, options);
- props.Add(key{{memberMeta.Name}}, value{{memberMeta.Name}});
+ var key{{member.Name}} = new MRubyValue(state.Intern({{member.Name}}KeyUtf8));
+ var value{{member.Name}} = options.Resolver.GetFormatterWithVerify<{{member.FullTypeName}}>()
+ .Serialize(value.{{member.Name}}, state, options);
+ props.Add(key{{member.Name}}, value{{member.Name}});
""");
}
@@ -270,61 +170,19 @@ public MRubyValue Serialize({{inputType}} value, MRubyState state, MRubyValueSer
}
""");
- return true;
}
- static bool TryEmitDeserializeMethod(
- MRubyObjectTypeMeta typeMeta,
- StringBuilder stringBuilder,
- ReferenceSymbols references,
- in SourceProductionContext context)
+ static void EmitDeserializeMethod(MRubyObjectModel model, StringBuilder stringBuilder)
{
- if (!TryGetConstructor(typeMeta, references, in context,
- out var selectedConstructor,
- out var constructedMembers))
- {
- return false;
- }
-
- var setterMembers = typeMeta.MemberMetas
- .Where(x =>
- {
- return constructedMembers.All(constructedMember => !SymbolEqualityComparer.Default.Equals(x.Symbol, constructedMember.Symbol));
- })
- .ToArray();
-
- foreach (var setterMember in setterMembers)
- {
- switch (setterMember)
- {
- case { IsProperty: true, IsSettable: false }:
- {
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.MRubyObjectPropertyMustHaveSetter,
- setterMember.GetLocation(typeMeta.Syntax),
- typeMeta.TypeName,
- setterMember.Name));
- return false;
- }
- case { IsField: true, IsSettable: false }:
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.MRubyObjectFieldCannotBeReadonly,
- setterMember.GetLocation(typeMeta.Syntax),
- typeMeta.TypeName,
- setterMember.Name));
- return false;
- }
- }
-
- var returnType = typeMeta.Symbol.IsValueType
- ? typeMeta.FullTypeName
- : $"{typeMeta.FullTypeName}?";
+ var returnType = model.IsValueType
+ ? model.FullTypeName
+ : $"{model.FullTypeName}?";
stringBuilder.AppendLine($$"""
[global::ChibiRuby.Serializer.Preserve]
public {{returnType}} Deserialize(MRubyValue value, MRubyState state, MRubyValueSerializerOptions options)
{
""");
- if (!typeMeta.Symbol.IsValueType)
+ if (!model.IsValueType)
{
stringBuilder.AppendLine($$"""
if (value.IsNil) return default;
@@ -332,24 +190,24 @@ static bool TryEmitDeserializeMethod(
""");
}
- if (typeMeta.MemberMetas.Count <= 0)
+ if (model.Members.Count <= 0)
{
stringBuilder.AppendLine($$"""
- return new {{typeMeta.TypeName}}();
+ return new {{model.TypeName}}();
}
""");
- return true;
+ return;
}
stringBuilder.AppendLine($$"""
- MRubySerializationException.ThrowIfTypeMismatch(value, MRubyVType.Hash, "{{typeMeta.TypeName}}");
+ MRubySerializationException.ThrowIfTypeMismatch(value, MRubyVType.Hash, "{{model.TypeName}}");
""");
- foreach (var memberMeta in typeMeta.MemberMetas)
+ foreach (var member in model.Members)
{
stringBuilder.AppendLine($$"""
- var __{{memberMeta.Name}}__ = {{memberMeta.EmitDefaultValue()}};
- var __{{memberMeta.Name}}NameSymbol__ = state.Intern({{memberMeta.Name}}KeyUtf8);
+ var __{{member.Name}}__ = {{member.DefaultValueExpr}};
+ var __{{member.Name}}NameSymbol__ = state.Intern({{member.Name}}KeyUtf8);
""");
}
@@ -359,13 +217,13 @@ static bool TryEmitDeserializeMethod(
{
""");
var branching = "if";
- foreach (var memberMeta in typeMeta.MemberMetas)
+ foreach (var member in model.Members)
{
stringBuilder.AppendLine($$"""
- {{branching}} (entry.Key.SymbolValue == __{{memberMeta.Name}}NameSymbol__)
+ {{branching}} (entry.Key.SymbolValue == __{{member.Name}}NameSymbol__)
{
- __{{memberMeta.Name}}__ = options.Resolver
- .GetFormatterWithVerify<{{memberMeta.FullTypeName}}>()
+ __{{member.Name}}__ = options.Resolver
+ .GetFormatterWithVerify<{{member.FullTypeName}}>()
.Deserialize(entry.Value, state, options);
continue;
}
@@ -377,31 +235,29 @@ static bool TryEmitDeserializeMethod(
""");
stringBuilder.Append($$"""
- return new {{typeMeta.TypeName}}
+ return new {{model.TypeName}}
""");
- if (selectedConstructor != null)
+ if (model.HasSelectedConstructor)
{
- var parameters = string.Join(",", constructedMembers.Select(x => $"__{x.Name}__"));
+ var parameters = string.Join(",", model.ConstructorParameterNames.Select(x => $"__{x}__"));
stringBuilder.AppendLine($"({parameters})");
}
else
{
stringBuilder.AppendLine("()");
}
+
+ var setterMembers = model.Members.Where(x => !x.IsConstructorParameter).ToArray();
if (setterMembers.Length > 0)
{
stringBuilder.AppendLine($$"""
{
""");
-
foreach (var setterMember in setterMembers)
{
- if (!constructedMembers.Contains(setterMember))
- {
- stringBuilder.AppendLine($$"""
+ stringBuilder.AppendLine($$"""
{{setterMember.Name}} = __{{setterMember.Name}}__,
""");
- }
}
stringBuilder.AppendLine($$"""
}
@@ -411,88 +267,5 @@ static bool TryEmitDeserializeMethod(
;
}
""");
- return true;
- }
-
- static bool TryGetConstructor(
- MRubyObjectTypeMeta typeMeta,
- ReferenceSymbols reference,
- in SourceProductionContext context,
- out IMethodSymbol? selectedConstructor,
- out IReadOnlyList constructedMembers)
- {
- if (typeMeta.Constructors.Count <= 0)
- {
- selectedConstructor = null;
- constructedMembers = [];
- return true;
- }
-
- if (typeMeta.Constructors.Count == 1)
- {
- selectedConstructor = typeMeta.Constructors[0];
- }
- else
- {
- var ctorWithAttrs = typeMeta.Constructors
- .Where(x => x.ContainsAttribute(reference.MRubyConstructorAttribute))
- .ToArray();
-
- switch (ctorWithAttrs.Length)
- {
- case 1:
- selectedConstructor = ctorWithAttrs[0];
- break;
- case > 1:
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.MultipleConstructorAttribute,
- typeMeta.Syntax.Identifier.GetLocation(),
- typeMeta.Symbol.Name));
- selectedConstructor = null;
- constructedMembers = [];
- return false;
-
- default:
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.MultipleConstructorWithoutAttribute,
- typeMeta.Syntax.Identifier.GetLocation(),
- typeMeta.Symbol.Name));
- selectedConstructor = null;
- constructedMembers = [];
- return false;
- }
- }
-
- var parameterMembers = new List();
- var error = false;
- foreach (var parameter in selectedConstructor.Parameters)
- {
- var matchedMember = typeMeta.MemberMetas
- .FirstOrDefault(member => parameter.Name.Equals(member.Name, StringComparison.OrdinalIgnoreCase));
- if (matchedMember != null)
- {
- matchedMember.IsConstructorParameter = true;
- if (parameter.HasExplicitDefaultValue)
- {
- matchedMember.HasExplicitDefaultValueFromConstructor = true;
- matchedMember.ExplicitDefaultValueFromConstructor = parameter.ExplicitDefaultValue;
- }
- parameterMembers.Add(matchedMember);
- }
- else
- {
- var location = selectedConstructor.Locations.FirstOrDefault() ??
- typeMeta.Syntax.Identifier.GetLocation();
- context.ReportDiagnostic(Diagnostic.Create(
- DiagnosticDescriptors.ConstructorHasNoMatchedParameter,
- location,
- typeMeta.Symbol.Name,
- parameter.Name));
- constructedMembers = [];
- error = true;
- }
- }
- constructedMembers = parameterMembers;
- return !error;
}
}
diff --git a/src/ChibiRuby.Serializer.SourceGenerator/MRubyObjectModel.cs b/src/ChibiRuby.Serializer.SourceGenerator/MRubyObjectModel.cs
new file mode 100644
index 00000000..a140480b
--- /dev/null
+++ b/src/ChibiRuby.Serializer.SourceGenerator/MRubyObjectModel.cs
@@ -0,0 +1,345 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace ChibiRuby.Serializer.SourceGenerator;
+
+///
+/// Value-equatable wrapper around so that records
+/// containing it participate correctly in the incremental generator cache.
+///
+readonly struct EquatableArray : IEquatable>, IEnumerable
+ where T : IEquatable
+{
+ public static readonly EquatableArray Empty = new(ImmutableArray.Empty);
+
+ readonly ImmutableArray array;
+
+ public EquatableArray(ImmutableArray array) => this.array = array;
+
+ public int Count => array.IsDefault ? 0 : array.Length;
+ public T this[int index] => array[index];
+
+ public bool Equals(EquatableArray other)
+ {
+ if (array.IsDefault) return other.array.IsDefault;
+ if (other.array.IsDefault) return false;
+ if (array.Length != other.array.Length) return false;
+ for (var i = 0; i < array.Length; i++)
+ {
+ if (!array[i].Equals(other.array[i])) return false;
+ }
+ return true;
+ }
+
+ public override bool Equals(object? obj) => obj is EquatableArray other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ if (array.IsDefault) return 0;
+ var hash = 17;
+ foreach (var item in array)
+ {
+ hash = unchecked(hash * 31 + (item?.GetHashCode() ?? 0));
+ }
+ return hash;
+ }
+
+ public IEnumerator GetEnumerator() =>
+ (array.IsDefault ? Enumerable.Empty() : array).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+}
+
+///
+/// Equatable, compilation-independent representation of a diagnostic source location,
+/// so diagnostics can be carried through the incremental cache and re-materialized later.
+///
+sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan)
+{
+ public Location ToLocation() => Location.Create(FilePath, TextSpan, LineSpan);
+
+ public static LocationInfo? CreateFrom(Location location)
+ {
+ if (location.Kind != LocationKind.SourceFile || location.SourceTree is null)
+ {
+ return null;
+ }
+ return new LocationInfo(
+ location.SourceTree.FilePath,
+ location.SourceSpan,
+ location.GetLineSpan().Span);
+ }
+}
+
+/// Equatable diagnostic descriptor + args that can be cached and reported at output time.
+sealed record DiagnosticInfo(
+ DiagnosticDescriptor Descriptor,
+ LocationInfo? Location,
+ EquatableArray MessageArgs) : IEquatable
+{
+ public Diagnostic ToDiagnostic()
+ {
+ var location = Location?.ToLocation() ?? Microsoft.CodeAnalysis.Location.None;
+ return Diagnostic.Create(Descriptor, location, MessageArgs.Cast