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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ For deeper, scoped detail see the nested files: [`src/PolyType/AGENTS.md`](src/P
## Project Layout

- **`src/`** — `PolyType` (core library: both providers, abstractions, attributes), `PolyType.Roslyn` (general-purpose `ITypeSymbol` → model extraction, reusable by third-party generators), `PolyType.SourceGenerator` (built-in incremental generator), `PolyType.Examples` (reference serializers/binders/mappers built on PolyType), `PolyType.TestCases` (+ `.FSharp`, shared test types), and `Shared` (polyfills/helpers).
- **`tests/`** — `PolyType.Tests` (main xUnit suite), `PolyType.Tests.NativeAOT` (AOT smoke tests, TUnit), `PolyType.SourceGenerator.UnitTests` (generator + snapshot tests), `PolyType.Roslyn.Tests`, `PolyType.Benchmarks` (BenchmarkDotNet; not a test project), and `SizeTrackingApp.AOT` (canonical app whose published size is tracked per-RID in `aot-size-baselines.json` via `make test-aot-size`).
- **`tests/`** — `PolyType.Tests` (main xUnit suite), `PolyType.Tests.NativeAOT` (AOT smoke tests, TUnit), `PolyType.SourceGenerator.UnitTests` (generator + snapshot tests), `PolyType.Roslyn.Tests`, `PolyType.Benchmarks` (BenchmarkDotNet; not a test project), and `SizeTrackingApp.AOT` (canonical .NET 11 JSON/configuration-binding app whose published size is tracked per-RID in `aot-size-baselines.json` via `make test-aot-size`).
- **`applications/`** — five Native AOT sample apps plus one reflection-based app.
- **`eng/AotSizeCheck/`** — internal CLI that backs `make test-aot-size`.

Expand Down
32 changes: 23 additions & 9 deletions docs/docs/core-abstractions.md
Original file line number Diff line number Diff line change
Expand Up @@ -558,10 +558,17 @@ class EmptyConstructorVisitor : TypeShapeVisitor
return new Func<TDeclaringType>(() =>
{
TArgumentState state = argumentStateCtor();
foreach (ParameterSetter<TArgumentState> parameterSetter in parameterSetters)
parameterSetter(ref state);
try
{
foreach (ParameterSetter<TArgumentState> parameterSetter in parameterSetters)
parameterSetter(ref state);

return ctor(state);
return ctor(state);
}
finally
{
state.Return();
}
});
}

Expand Down Expand Up @@ -726,14 +733,21 @@ partial class LoggingVisitor : TypeShapeVisitor
{
Console.WriteLine($"Invoking {methodShape.Name}");
TArgumentState argumentState = argumentStateCtor();
foreach (var parameterSetter in parameterSetters)
try
{
parameterSetter(ref argumentState, arguments);
}
foreach (var parameterSetter in parameterSetters)
{
parameterSetter(ref argumentState, arguments);
}

TResult result = await invoker(ref instance.Value, ref argumentState);
Console.WriteLine($"Completed {methodShape.Name} with result {result}");
return result;
TResult result = await invoker(ref instance.Value, ref argumentState);
Console.WriteLine($"Completed {methodShape.Name} with result {result}");
return result;
}
finally
{
argumentState.Return();
}
});
}

Expand Down
12 changes: 12 additions & 0 deletions eng/AotSizeCheck/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ This tool measures the file size of the Native AOT publish output of
compares it against the per-platform baselines committed in
[`tests/SizeTrackingApp.AOT/aot-size-baselines.json`](../../tests/SizeTrackingApp.AOT/aot-size-baselines.json).

The canary targets .NET 11 and uses the source-generated JSON serializer and
configuration binder over the connected commerce/service model in
[`RepresentativeModel.cs`](../../tests/SizeTrackingApp.AOT/RepresentativeModel.cs).
The model has 75 record types and five enums, spanning 135 generated shapes
including contextual union base shapes.
Both consumers verify a complete round trip, including the union cases and
surrogate identifier. The fixed model spans varied constructor signatures,
nullable values, enums, arrays, sets, and dictionaries so the measurement
captures generic-specialization costs across a representative shape graph.
Keep the model, consumers, and publish settings stable; deliberate changes
require new measurements for every platform baseline.

The check is bidirectional — it fails on both **growth** and significant
**shrinkage** so the baseline window can be ratcheted tighter over time
as PolyType gets leaner.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,34 +99,41 @@ internal sealed class CborObjectConverterWithParameterizedCtor<TDeclaringType, T

reader.ReadStartMap();
TArgumentState argumentState = createArgumentState();
Dictionary<string, CborPropertyConverter<TArgumentState>> ctorParams = _constructorParameters;

while (reader.PeekState() != CborReaderState.EndMap)
try
{
string key = reader.ReadTextString();
if (!ctorParams.TryGetValue(key, out CborPropertyConverter<TArgumentState>? propertyConverter))
Dictionary<string, CborPropertyConverter<TArgumentState>> ctorParams = _constructorParameters;

while (reader.PeekState() != CborReaderState.EndMap)
{
reader.SkipValue();
continue;
string key = reader.ReadTextString();
if (!ctorParams.TryGetValue(key, out CborPropertyConverter<TArgumentState>? propertyConverter))
{
reader.SkipValue();
continue;
}

if (argumentState.IsArgumentSet(propertyConverter.Position))
{
ThrowDuplicateProperty(key);
static void ThrowDuplicateProperty(string key) => throw CreateDuplicatePropertyException(key);
}

propertyConverter.Read(reader, ref argumentState);
Debug.Assert(argumentState.IsArgumentSet(propertyConverter.Position));
}

if (argumentState.IsArgumentSet(propertyConverter.Position))
reader.ReadEndMap();

if (!argumentState.AreRequiredArgumentsSet)
{
ThrowDuplicateProperty(key);
static void ThrowDuplicateProperty(string key) => throw CreateDuplicatePropertyException(key);
Helpers.ThrowMissingRequiredArguments(ref argumentState, parameters);
}

propertyConverter.Read(reader, ref argumentState);
Debug.Assert(argumentState.IsArgumentSet(propertyConverter.Position));
return createObject(ref argumentState);
}

reader.ReadEndMap();

if (!argumentState.AreRequiredArgumentsSet)
finally
{
Helpers.ThrowMissingRequiredArguments(ref argumentState, parameters);
argumentState.Return();
}

return createObject(ref argumentState);
}
}
15 changes: 11 additions & 4 deletions src/PolyType.Examples/Cloner/Cloner.Builder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,19 @@ private sealed class Builder(TypeGenerationContext generationContext) : TypeShap
}

TArgumentState state = argumentStateCtor();
foreach (PropertyCloner<TDeclaringType, TArgumentState> parameterMapper in parameterMappers)
try
{
parameterMapper(ref source, ref state);
}
foreach (PropertyCloner<TDeclaringType, TArgumentState> parameterMapper in parameterMappers)
{
parameterMapper(ref source, ref state);
}

return ctor(ref state);
return ctor(ref state);
}
finally
{
state.Return();
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,27 @@ private sealed class Builder(ITypeShapeFunc self) : TypeShapeVisitor, ITypeShape
}

TArgumentState argState = argStateCtor();
foreach ((string name, bool isRequired, PropertyBinder<TArgumentState> binder) in paramBinders)
try
{
if (configuration.GetSection(name) is { } section)
{
binder(ref argState, section);
}
else if (isRequired)
foreach ((string name, bool isRequired, PropertyBinder<TArgumentState> binder) in paramBinders)
{
Throw(name);
static void Throw(string name) => throw new InvalidOperationException($"Missing required configuration key '{name}'.");
if (configuration.GetSection(name) is { } section)
{
binder(ref argState, section);
}
else if (isRequired)
{
Throw(name);
static void Throw(string name) => throw new InvalidOperationException($"Missing required configuration key '{name}'.");
}
}
}

return paramCtor(ref argState);
return paramCtor(ref argState);
}
finally
{
argState.Return();
}
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using PolyType.Abstractions;
using PolyType.Examples.Utilities;
using PolyType.Utilities;
using System.Diagnostics;

Expand Down Expand Up @@ -62,11 +63,19 @@ private sealed class Builder(ServiceProviderContext serviceProviderCtx, ITypeSha
return ServiceFactory.FromFunc(provider =>
{
TArgumentState argumentState = argumentStateCtor();
foreach (var mapper in parameterMappers)
try
{
mapper(provider, ref argumentState);
foreach (var mapper in parameterMappers)
{
mapper(provider, ref argumentState);
}

return parameterizedCtor(ref argumentState);
}
finally
{
argumentState.Return();
}
return parameterizedCtor(ref argumentState);
}, lifetime);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,32 +110,38 @@ internal sealed class JsonObjectConverterWithParameterizedCtor<TDeclaringType, T

JsonPropertyDictionary<JsonPropertyConverter<TArgumentState>> ctorParams = _constructorParameters;
TArgumentState argumentState = createArgumentState();

while (reader.TokenType != JsonTokenType.EndObject)
try
{
Debug.Assert(reader.TokenType is JsonTokenType.PropertyName);
while (reader.TokenType != JsonTokenType.EndObject)
{
Debug.Assert(reader.TokenType is JsonTokenType.PropertyName);

JsonPropertyConverter<TArgumentState>? jsonProperty = ctorParams.LookupProperty(ref reader);
reader.EnsureRead();
JsonPropertyConverter<TArgumentState>? jsonProperty = ctorParams.LookupProperty(ref reader);
reader.EnsureRead();

if (jsonProperty != null)
{
jsonProperty.Read(ref reader, ref argumentState, options);
if (jsonProperty != null)
{
jsonProperty.Read(ref reader, ref argumentState, options);
}
else
{
reader.Skip();
}

reader.EnsureRead();
}
else

if (!argumentState.AreRequiredArgumentsSet)
{
reader.Skip();
Helpers.ThrowMissingRequiredArguments(ref argumentState, parameters);
}

reader.EnsureRead();
return createObject(ref argumentState);
}

if (!argumentState.AreRequiredArgumentsSet)
finally
{
Helpers.ThrowMissingRequiredArguments(ref argumentState, parameters);
argumentState.Return();
}

return createObject(ref argumentState);
}
}

Expand Down
25 changes: 16 additions & 9 deletions src/PolyType.Examples/JsonSerializer/JsonSerializer.Builder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PolyType.Abstractions;
using PolyType.Examples.JsonSerializer.Converters;
using PolyType.Examples.Utilities;
using PolyType.Utilities;
using System.Diagnostics;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -297,19 +298,25 @@ public JsonConverter<T> GetOrAddConverter<T>(ITypeShape<T> shape) =>
return new JsonFunc(async (parameters, cancellationToken) =>
{
TArgumentState argumentState = argumentStateCtor();

foreach (var setter in parameterSetters)
try
{
setter(ref argumentState, parameters, cancellationToken);
}
foreach (var setter in parameterSetters)
{
setter(ref argumentState, parameters, cancellationToken);
}

if (!argumentState.AreRequiredArgumentsSet)
if (!argumentState.AreRequiredArgumentsSet)
{
ThrowMissingRequiredArguments(ref argumentState);
}

TResult result = await invoker(ref boxedTarget.Value, ref argumentState).ConfigureAwait(false);
return resultConverter.SerializeToElement(result);
}
finally
{
ThrowMissingRequiredArguments(ref argumentState);
argumentState.Return();
}

TResult result = await invoker(ref boxedTarget.Value, ref argumentState).ConfigureAwait(false);
return resultConverter.SerializeToElement(result);
});

void ThrowMissingRequiredArguments(ref TArgumentState argumentState)
Expand Down
15 changes: 11 additions & 4 deletions src/PolyType.Examples/ObjectMapper/Mapper.Builder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,19 @@ private sealed class TypeScopedVisitor<TSource>(Builder baseVisitor) : TypeShape
}

TArgumentState argumentState = argumentStateCtor();
foreach (PropertyMapper<TSource, TArgumentState> mapper in propertyMappers)
try
{
mapper(ref source, ref argumentState);
}
foreach (PropertyMapper<TSource, TArgumentState> mapper in propertyMappers)
{
mapper(ref source, ref argumentState);
}

return ctor(ref argumentState);
return ctor(ref argumentState);
}
finally
{
argumentState.Return();
}
});
}
}
Expand Down
17 changes: 12 additions & 5 deletions src/PolyType.Examples/RandomGenerator/RandomGenerator.Builder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,21 @@ private sealed class Builder(ITypeShapeFunc self) : TypeShapeVisitor, ITypeShape
}

TArgumentState argState = argumentStateCtor();
int propertySize = GetChildSize(size, parameterSetters.Length);
try
{
int propertySize = GetChildSize(size, parameterSetters.Length);

foreach (var parameterSetter in parameterSetters)
{
parameterSetter(ref argState, random, propertySize);
}

foreach (var parameterSetter in parameterSetters)
return ctor(ref argState);
}
finally
{
parameterSetter(ref argState, random, propertySize);
argState.Return();
}

return ctor(ref argState);
});
}
}
Expand Down
Loading
Loading