Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MemberIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,12 @@ internal sealed class MemberIdTable
{
private readonly List<string> _declarations = new();
private readonly Dictionary<string, int> _usedIdentifiers = new();
private readonly List<(Property Property, string FieldName)> _propertyGetterAccessFields = new();

internal Dictionary<Method, int> MethodIds { get; } = new();
internal Dictionary<Property, int> PropertyGetIds { get; } = new();
internal Dictionary<Property, int> PropertySetIds { get; } = new();
internal Dictionary<Property, string> PropertyGetterAccessFieldNames { get; } = new();
internal Dictionary<Property, int> IndexerGetIds { get; } = new();
internal Dictionary<Property, int> IndexerSetIds { get; } = new();
internal Dictionary<Event, int> EventSubscribeIds { get; } = new();
Expand Down Expand Up @@ -190,11 +192,26 @@ internal void AddProperty(Property property)
int getId = AllocateId("MemberId_" + identifierGet);
PropertyGetIds[property] = getId;

// PropertyGetterAccess is identified solely by Name, so the generator emits one
// static readonly per-property singleton next to MemberId_<X>_Get and reuses it for
// every recorded access. Sharing one reference is safe because the only two
// reference-keyed verification paths in the codebase tolerate it: the
// FastMockInteractions._verified filter is all-or-nothing per matched property
// (every recording of a getter shares the same predicate result), and
// VerificationResultExtensions.Then walks the snapshot positionally so repeated
// occurrences of the same reference still resolve to distinct positions.
string accessFieldName = "PropertyAccess_" + identifierGet;
PropertyGetterAccessFieldNames[property] = accessFieldName;
_propertyGetterAccessFields.Add((property, accessFieldName));

string identifierSet = UniqueIdentifier(property.Name + "_Set");
int setId = AllocateId("MemberId_" + identifierSet);
PropertySetIds[property] = setId;
}

internal string GetPropertyGetterAccessFieldName(Property property)
=> PropertyGetterAccessFieldNames[property];

internal void AddIndexer(Property indexer)
{
string keySignature = BuildIndexerSignatureSuffix(indexer);
Expand Down Expand Up @@ -241,6 +258,16 @@ internal void Emit(StringBuilder sb, string indent)

sb.Append(indent).Append("internal const int MemberCount = ").Append(_declarations.Count)
.Append(';').AppendLine();

foreach ((Property property, string fieldName) in _propertyGetterAccessFields)
{
sb.Append(indent)
.Append(
"internal static readonly global::Mockolate.Interactions.PropertyGetterAccess ")
.Append(fieldName)
.Append(" = new global::Mockolate.Interactions.PropertyGetterAccess(")
.Append(property.GetUniqueNameString()).Append(");").AppendLine();
}
}

private static string BuildIndexerSignatureSuffix(Property indexer)
Expand Down
18 changes: 11 additions & 7 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1331,9 +1331,10 @@ private static void AppendCreateFastInteractions(StringBuilder sb, string indent
}

string getMemberIdRef = memberIdPrefix + memberIds.GetPropertyGetIdentifier(property);
string accessFieldRef = memberIdPrefix + memberIds.GetPropertyGetterAccessFieldName(property);
sb.Append(indent)
.Append("\tglobal::Mockolate.Interactions.FastPropertyBufferFactory.InstallPropertyGetter(fast, ")
.Append(getMemberIdRef).Append(");").AppendLine();
.Append(getMemberIdRef).Append(", ").Append(accessFieldRef).Append(");").AppendLine();

string setMemberIdRef = memberIdPrefix + memberIds.GetPropertySetIdentifier(property);
string propertyType = property.Type.ToTypeOrWrapper();
Expand Down Expand Up @@ -2334,12 +2335,13 @@ property.IndexerParameters is not null
}
else
{
string accessFieldRef = memberIdPrefix + memberIds.GetPropertyGetterAccessFieldName(property);
if (useFastForProperty)
{
sb.Append("\t\t\t\treturn ").Append(mockRegistry).Append(".GetPropertyFast<")
.AppendTypeOrWrapper(property.Type).Append(">(")
.Append(memberIdPrefix).Append(memberIds.GetPropertyGetIdentifier(property))
.Append(", ").Append(property.GetUniqueNameString()).Append(", static b => ")
.Append(", ").Append(accessFieldRef).Append(", static b => ")
.AppendDefaultValueGeneratorFor(property.Type, "b.DefaultValue");
if (!property.IsStatic)
{
Expand All @@ -2353,7 +2355,7 @@ property.IndexerParameters is not null
{
sb.Append("\t\t\t\treturn ").Append(mockRegistry).Append(".GetProperty<")
.AppendTypeOrWrapper(property.Type).Append(">(")
.Append(propertyGetMemberArg).Append(property.GetUniqueNameString()).Append(", () => ")
.Append(propertyGetMemberArg).Append(accessFieldRef).Append(", () => ")
.AppendDefaultValueGeneratorFor(property.Type, $"{mockRegistry}.Behavior.DefaultValue");
if (!property.IsStatic)
{
Expand Down Expand Up @@ -2423,12 +2425,13 @@ property.IndexerParameters is not null
}
else
{
string accessFieldRef = memberIdPrefix + memberIds.GetPropertyGetterAccessFieldName(property);
if (useFastForProperty)
{
sb.Append("\t\t\t\treturn ").Append(mockRegistry).Append(".GetPropertyFast<")
.AppendTypeOrWrapper(property.Type).Append(">(")
.Append(memberIdPrefix).Append(memberIds.GetPropertyGetIdentifier(property))
.Append(", ").Append(property.GetUniqueNameString()).Append(", static b => ")
.Append(", ").Append(accessFieldRef).Append(", static b => ")
.AppendDefaultValueGeneratorFor(property.Type, "b.DefaultValue");
if (property is { IsStatic: false, } && property.Getter?.IsProtected != true)
{
Expand All @@ -2447,7 +2450,7 @@ property.IndexerParameters is not null
{
sb.Append("\t\t\t\treturn ").Append(mockRegistry).Append(".GetProperty<")
.AppendTypeOrWrapper(property.Type).Append(">(")
.Append(propertyGetMemberArg).Append(property.GetUniqueNameString()).Append(", () => ")
.Append(propertyGetMemberArg).Append(accessFieldRef).Append(", () => ")
.AppendDefaultValueGeneratorFor(property.Type, $"{mockRegistry}.Behavior.DefaultValue");
if (property is { IsStatic: false, } && property.Getter?.IsProtected != true)
{
Expand Down Expand Up @@ -2485,12 +2488,13 @@ property.IndexerParameters is not null
}
else
{
string accessFieldRef = memberIdPrefix + memberIds.GetPropertyGetterAccessFieldName(property);
if (useFastForProperty)
{
sb.Append("\t\t\t\treturn ").Append(mockRegistry).Append(".GetPropertyFast<")
.AppendTypeOrWrapper(property.Type).Append(">(")
.Append(memberIdPrefix).Append(memberIds.GetPropertyGetIdentifier(property))
.Append(", ").Append(property.GetUniqueNameString())
.Append(", ").Append(accessFieldRef)
.Append(", static b => ")
.AppendDefaultValueGeneratorFor(property.Type, "b.DefaultValue")
.Append(");").AppendLine();
Expand All @@ -2499,7 +2503,7 @@ property.IndexerParameters is not null
{
sb.Append("\t\t\t\treturn ").Append(mockRegistry).Append(".GetProperty<")
.AppendTypeOrWrapper(property.Type).Append(">(")
.Append(propertyGetMemberArg).Append(property.GetUniqueNameString())
.Append(propertyGetMemberArg).Append(accessFieldRef)
.Append(", () => ")
.AppendDefaultValueGeneratorFor(property.Type, $"{mockRegistry}.Behavior.DefaultValue")
.Append(", null);").AppendLine();
Expand Down
83 changes: 72 additions & 11 deletions Source/Mockolate/Interactions/FastPropertyBuffer.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Mockolate.Parameters;

namespace Mockolate.Interactions;

/// <summary>
/// Per-member buffer for property getters. Records only the property name + sequence number.
/// Per-member buffer for property getters. The buffer is bound to a single property; every
/// recorded record stores only a sequence number and shares a single
/// <see cref="PropertyGetterAccess" /> when boxed for verification. Sharing one reference
/// across records is safe because property getters carry no parameters — every recorded
/// access is semantically identical — and the two reference-keyed verification paths in
/// this codebase tolerate it: <c>FastMockInteractions._verified</c> filters all-or-nothing
/// per matched property, and <c>VerificationResultExtensions.Then</c> walks the snapshot
/// positionally so repeated occurrences of the same reference still resolve to distinct
/// positions.
/// </summary>
[DebuggerDisplay("{Count} property gets")]
#if !DEBUG
Expand All @@ -15,26 +24,40 @@ public sealed class FastPropertyGetterBuffer : IFastMemberBuffer
{
private readonly FastMockInteractions _owner;
private readonly ChunkedSlotStorage<Record> _storage = new();
private PropertyGetterAccess? _access;

internal FastPropertyGetterBuffer(FastMockInteractions owner)
{
_owner = owner;
}

internal FastPropertyGetterBuffer(FastMockInteractions owner, PropertyGetterAccess access)
{
_owner = owner;
_access = access;
}

/// <inheritdoc cref="IFastMemberBuffer.Count" />
public int Count => _storage.Count;

/// <summary>
/// Records a property getter access.
/// Records a property getter access using the buffer's pre-seeded
/// <see cref="PropertyGetterAccess" /> singleton. Throws when the singleton has not been
/// installed — callers must use <see cref="Append(string)" /> in that case.
/// </summary>
public void Append(string name)
/// <exception cref="InvalidOperationException">No singleton was supplied at install time.</exception>
public void Append()
{
if (_access is null)
{
throw new InvalidOperationException(
$"{nameof(Append)}() requires the buffer to be installed with a {nameof(PropertyGetterAccess)} singleton via {nameof(FastPropertyBufferFactory)}.{nameof(FastPropertyBufferFactory.InstallPropertyGetter)}(memberId, access). Use {nameof(Append)}(string) when no singleton is available.");
}

long seq = _owner.NextSequence();
int slot = _storage.Reserve();
ref Record r = ref _storage.SlotForWrite(slot);
r.Seq = seq;
r.Name = name;
r.Boxed = null;
_storage.Publish();
Comment thread
vbreuss marked this conversation as resolved.

if (_owner.HasInteractionAddedSubscribers)
Expand All @@ -43,6 +66,22 @@ public void Append(string name)
}
}

/// <summary>
/// Records a property getter access. Lazily installs the buffer's
/// <see cref="PropertyGetterAccess" /> singleton from <paramref name="name" /> on first
/// call so legacy callers (generated code that does not pass a pre-built singleton) keep
/// working without allocating one access object per record.
/// </summary>
public void Append(string name)
{
// Lazy init: the buffer is bound to a single property, so one singleton covers every
// record. The benign race here is acceptable — both instances are equivalent because
// PropertyGetterAccess is identified solely by Name; whichever assignment wins still
// satisfies the contract.
_access ??= new PropertyGetterAccess(name);
Append();
}

/// <inheritdoc cref="IFastMemberBuffer.Clear" />
public void Clear() => _storage.Clear();

Expand All @@ -51,11 +90,16 @@ void IFastMemberBuffer.AppendBoxed(List<(long Seq, IInteraction Interaction)> de
lock (_storage.Lock)
{
int n = _storage.PublishedUnderLock;
if (n == 0)
{
return;
}

PropertyGetterAccess access = _access!;
for (int slot = 0; slot < n; slot++)
{
ref Record r = ref _storage.SlotUnderLock(slot);
r.Boxed ??= new PropertyGetterAccess(r.Name);
dest.Add((r.Seq, r.Boxed));
dest.Add((r.Seq, access));
}
}
}
Expand All @@ -65,6 +109,12 @@ void IFastMemberBuffer.AppendBoxedUnverified(List<(long Seq, IInteraction Intera
lock (_storage.Lock)
{
int n = _storage.PublishedUnderLock;
if (n == 0)
{
return;
}

PropertyGetterAccess access = _access!;
for (int slot = 0; slot < n; slot++)
{
if (_storage.VerifiedUnderLock(slot))
Expand All @@ -73,8 +123,7 @@ void IFastMemberBuffer.AppendBoxedUnverified(List<(long Seq, IInteraction Intera
}

ref Record r = ref _storage.SlotUnderLock(slot);
r.Boxed ??= new PropertyGetterAccess(r.Name);
dest.Add((r.Seq, r.Boxed));
dest.Add((r.Seq, access));
}
}
}
Expand Down Expand Up @@ -103,8 +152,6 @@ public int ConsumeMatching()
internal struct Record
{
public long Seq;
public string Name;
public IInteraction? Boxed;
}
}

Expand Down Expand Up @@ -234,6 +281,20 @@ public static FastPropertyGetterBuffer InstallPropertyGetter(this FastMockIntera
return buffer;
}

/// <summary>
/// Creates and installs a property getter buffer at the given <paramref name="memberId" /> with
/// a pre-built shared <paramref name="access" /> singleton. Used by the source generator so the
/// buffer never has to allocate a <see cref="PropertyGetterAccess" /> on the first record or on
/// verification.
Comment thread
vbreuss marked this conversation as resolved.
/// </summary>
public static FastPropertyGetterBuffer InstallPropertyGetter(this FastMockInteractions interactions,
int memberId, PropertyGetterAccess access)
{
FastPropertyGetterBuffer buffer = new(interactions, access);
interactions.InstallBuffer(memberId, buffer);
return buffer;
}

/// <summary>
/// Creates and installs a property setter buffer at the given <paramref name="memberId" />.
/// </summary>
Expand Down
Loading