Skip to content

Commit 58f3088

Browse files
AnthonyLloydCopilot
andcommitted
feat: AOT compatibility - move collection properties to extension methods
Move Array, Array2D, List, HashSet, and ArrayUnique from Gen<T> instance properties to extension methods on the static Gen class. This prevents infinite generic type expansion during NativeAOT compilation (ILC) when Gen<T> is instantiated for self-referential types. BREAKING CHANGE: .Array, .Array2D, .List, .HashSet, .ArrayUnique are now methods requiring parentheses: .Array(), .Array2D(), .List(), .HashSet(), .ArrayUnique(). Indexer syntax is unchanged: .Array()[0, 5]. - Add IsAotCompatible to CsCheck.csproj - Update all call sites in library, tests, and documentation - Verified: 299 tests pass, NativeAOT compiles including recursive types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0ca2947 commit 58f3088

20 files changed

Lines changed: 137 additions & 136 deletions

CsCheck/Check.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Anthony Lloyd
1+
// Copyright 2026 Anthony Lloyd
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -1408,7 +1408,7 @@ public static void SampleModelBased<Actual, Model>(this Gen<(Actual, Model)> ini
14081408
}
14091409

14101410
new GenInitial<Actual, Model>(initial)
1411-
.Select(Gen.OneOf(opNameActions).Array, (a, b) => new ModelBasedData<Actual, Model>(a.Actual, a.Model, a.Stream, a.Seed, b))
1411+
.Select(Gen.OneOf(opNameActions).Array(), (a, b) => new ModelBasedData<Actual, Model>(a.Actual, a.Model, a.Stream, a.Seed, b))
14121412
.Sample(d =>
14131413
{
14141414
try
@@ -1754,7 +1754,7 @@ public static void SampleParallel<T>(this Gen<T> initial, GenOperation<T>[] oper
17541754
Gen.Int[2, maxParallelOperations]
17551755
.SelectMany(np => Gen.Int[2, Math.Min(threads, np)].Select(nt => (nt, np)))
17561756
.SelectMany((nt, np) => Gen.Int[0, maxSequentialOperations].Select(ns => (ns, nt, np)))
1757-
.SelectMany((ns, nt, np) => new GenSampleParallel<T>(initial).Select(genOps.Array[ns], genOps.Array[np])
1757+
.SelectMany((ns, nt, np) => new GenSampleParallel<T>(initial).Select(genOps.Array()[ns], genOps.Array()[np])
17581758
.Select((initial, sequential, parallel) => (initial, sequential, nt, parallel)))
17591759
.Select((initial, sequential, threads, parallel) => new SampleParallelData<T>(initial.Value, initial.Stream, initial.Seed, sequential, parallel, threads))
17601760
.Sample(spd =>
@@ -2018,7 +2018,7 @@ public static void SampleParallel<Actual, Model>(this Gen<(Actual, Model)> initi
20182018
Gen.Int[2, maxParallelOperations]
20192019
.SelectMany(np => Gen.Int[2, Math.Min(threads, np)].Select(nt => (nt, np)))
20202020
.SelectMany((nt, np) => Gen.Int[0, maxSequentialOperations].Select(ns => (ns, nt, np)))
2021-
.SelectMany((ns, nt, np) => new GenSampleParallel<Actual, Model>(initial).Select(genOps.Array[ns], genOps.Array[np])
2021+
.SelectMany((ns, nt, np) => new GenSampleParallel<Actual, Model>(initial).Select(genOps.Array()[ns], genOps.Array()[np])
20222022
.Select((initial, sequential, parallel) => (initial, sequential, nt, parallel)))
20232023
.Select((initial, sequential, threads, parallel) => new SampleParallelData<Actual, Model>(initial.Actual, initial.Model, initial.Stream, initial.Seed, sequential, parallel, threads))
20242024
.Sample(spd =>

CsCheck/CsCheck.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Fixed Faster statistics locking.
4444
<NoWarn>CS1591,MA0143</NoWarn>
4545
<PackageReadmeFile>README.md</PackageReadmeFile>
4646
<AnalysisMode>All</AnalysisMode>
47+
<IsAotCompatible>true</IsAotCompatible>
4748
</PropertyGroup>
4849
<ItemGroup>
4950
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />

CsCheck/Gen.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Anthony Lloyd
1+
// Copyright 2026 Anthony Lloyd
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -91,24 +91,24 @@ public abstract class Gen<T> : IGen<T>
9191
public GenOperation<Actual, Model> Operation<Actual, Model>(Action<Actual, T> actual, Action<Model, T> model) => GenOperation.Create(this, actual, model);
9292
public GenMetamorphic<S> Metamorphic<S>(Func<T, string> name, Action<S, T> action1, Action<S, T> action2) => GenMetamorphic.Create(this, name, action1, action2);
9393
public GenMetamorphic<S> Metamorphic<S>(Action<S, T> action1, Action<S, T> action2) => GenMetamorphic.Create(this, Check.Print, action1, action2);
94-
95-
/// <summary>Generator for an array of <typeparamref name="T"/></summary>
96-
public GenArray<T> Array => new(this);
97-
/// <summary>Generator for a two dimensional array of <typeparamref name="T"/></summary>
98-
public GenArray2D<T> Array2D => new(this);
99-
/// <summary>Generator for a List of <typeparamref name="T"/></summary>
100-
public GenList<T> List => new(this);
101-
/// <summary>Generator for a HashSet of <typeparamref name="T"/></summary>
102-
public GenHashSet<T> HashSet => new(this);
103-
/// <summary>Generator for a unique array of <typeparamref name="T"/></summary>
104-
public GenArrayUnique<T> ArrayUnique => new(this);
10594
}
10695

10796
public delegate T GenMap<T>(T v, ref Size size);
10897

10998
/// <summary>Provides a set of static methods for composing generators.</summary>
11099
public static class Gen
111100
{
101+
/// <summary>Generator for an array of <typeparamref name="T"/></summary>
102+
public static GenArray<T> Array<T>(this Gen<T> gen) => new(gen);
103+
/// <summary>Generator for a two dimensional array of <typeparamref name="T"/></summary>
104+
public static GenArray2D<T> Array2D<T>(this Gen<T> gen) => new(gen);
105+
/// <summary>Generator for a List of <typeparamref name="T"/></summary>
106+
public static GenList<T> List<T>(this Gen<T> gen) => new(gen);
107+
/// <summary>Generator for a HashSet of <typeparamref name="T"/></summary>
108+
public static GenHashSet<T> HashSet<T>(this Gen<T> gen) => new(gen);
109+
/// <summary>Generator for a unique array of <typeparamref name="T"/></summary>
110+
public static GenArrayUnique<T> ArrayUnique<T>(this Gen<T> gen) => new(gen);
111+
112112
sealed class GenConst<T>(T value) : Gen<T>
113113
{
114114
public override T Generate(PCG pcg, Size? min, out Size size)
@@ -2683,7 +2683,7 @@ public override char Generate(PCG pcg, Size? min, out Size size)
26832683

26842684
public sealed class GenString : Gen<string>
26852685
{
2686-
static readonly Gen<string> d = Gen.Char.Array.Select(i => new string(i));
2686+
static readonly Gen<string> d = Gen.Char.Array().Select(i => new string(i));
26872687
public override string Generate(PCG pcg, Size? min, out Size size)
26882688
=> d.Generate(pcg, min, out size);
26892689
/// <summary>Generate string with length in the range <paramref name="start"/> to <paramref name="finish"/> both inclusive.</summary>
@@ -2692,7 +2692,7 @@ public override string Generate(PCG pcg, Size? min, out Size size)
26922692
get
26932693
{
26942694
if (finish < start) ThrowHelper.ThrowFinishLessThanStart(start, finish);
2695-
return Gen.Char.Array[start, finish].Select(i => new string(i));
2695+
return Gen.Char.Array()[start, finish].Select(i => new string(i));
26962696
}
26972697
}
26982698

@@ -2701,16 +2701,16 @@ public override string Generate(PCG pcg, Size? min, out Size size)
27012701
get
27022702
{
27032703
if (finish < start) ThrowHelper.ThrowFinishLessThanStart(start, finish);
2704-
return gen.Array[start, finish].Select(i => new string(i));
2704+
return gen.Array()[start, finish].Select(i => new string(i));
27052705
}
27062706
}
27072707

27082708
public Gen<string> this[Gen<char> gen] =>
2709-
gen.Array.Select(i => new string(i));
2709+
gen.Array().Select(i => new string(i));
27102710
/// <summary>Generate string from chars in the string.</summary>
27112711
public Gen<string> this[string chars] =>
2712-
Gen.Char[chars].Array.Select(i => new string(i));
2713-
public readonly Gen<string> AlphaNumeric = Gen.Char.AlphaNumeric.Array.Select(i => new string(i));
2712+
Gen.Char[chars].Array().Select(i => new string(i));
2713+
public readonly Gen<string> AlphaNumeric = Gen.Char.AlphaNumeric.Array().Select(i => new string(i));
27142714
}
27152715

27162716
public sealed class GenSeed : Gen<string>

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ The following tests are in ~~xUnit~~ TUnit but could equally be used in any test
3535

3636
More to see in the [Tests](https://github.com/AnthonyLloyd/CsCheck/tree/master/Tests). There are also 1,000+ F# tests using CsCheck in [MKL.NET](https://github.com/MKL-NET/MKL.NET/tree/master/Tests).
3737

38-
No Reflection was used in the making of this product. CsCheck is close to being AOT compatible but 'generic recursion is AOT kryptonite'.
38+
No Reflection was used in the making of this product. CsCheck is AOT compatible.
3939

4040
## Generator Creation Example
4141

@@ -67,7 +67,7 @@ static readonly Gen<JsonNode> genJsonNode = Gen.Recursive<JsonNode>((depth, genJ
6767
{
6868
if (depth == 5) return genJsonValue;
6969
var genJsonObject = Gen.Dictionary(genString, genJsonNode.Null())[0, 5].Select(d => new JsonObject(d));
70-
var genJsonArray = genJsonNode.Null().Array[0, 5].Select(i => new JsonArray(i));
70+
var genJsonArray = genJsonNode.Null().Array()[0, 5].Select(i => new JsonArray(i));
7171
return Gen.OneOf(genJsonObject, genJsonArray, genJsonValue);
7272
});
7373
```
@@ -110,7 +110,7 @@ public void Int_Distribution()
110110
int buckets = 70;
111111
int frequency = 10;
112112
int[] expected = Enumerable.Repeat(frequency, buckets).ToArray();
113-
Gen.Int[0, buckets - 1].Array[frequency * buckets]
113+
Gen.Int[0, buckets - 1].Array()[frequency * buckets]
114114
.Select(sample => Tally(buckets, sample))
115115
.Sample(actual => Check.ChiSquared(expected, actual));
116116
}
@@ -150,7 +150,7 @@ public void DateTime()
150150
[Test]
151151
public void No2_LargeUnionList()
152152
{
153-
Gen.Int.Array.Array
153+
Gen.Int.Array().Array()
154154
.Sample(aa =>
155155
{
156156
var hs = new HashSet<int>();
@@ -173,7 +173,7 @@ public void RecursiveDepth()
173173
{
174174
int maxDepth = 4;
175175
Gen.Recursive<MyObj>((i, my) =>
176-
Gen.Select(Gen.Int, my.Array[0, i < maxDepth ? 6 : 0], (i, a) => new MyObj(i, a))
176+
Gen.Select(Gen.Int, my.Array()[0, i < maxDepth ? 6 : 0], (i, a) => new MyObj(i, a))
177177
)
178178
.Sample(i =>
179179
{
@@ -194,8 +194,8 @@ public void AllocatorMany_Classify()
194194
{
195195
Gen.Select(Gen.Int[3, 30], Gen.Int[3, 15]).SelectMany((rows, cols) =>
196196
Gen.Select(
197-
Gen.Int[0, 5].Array[cols].Where(a => a.Sum() > 0).Array[rows],
198-
Gen.Int[900, 1000].Array[rows],
197+
Gen.Int[0, 5].Array()[cols].Where(a => a.Sum() > 0).Array()[rows],
198+
Gen.Int[900, 1000].Array()[rows],
199199
Gen.Int.Uniform))
200200
.Sample((solution,
201201
rowPrice,
@@ -234,7 +234,7 @@ SampleModelBased generates an initial actual and model and then applies a random
234234
public void
235235
SetSlim_ModelBased()
236236
{
237-
Gen.Int.Array.Select(a => (new SetSlim<int>(a), new HashSet<int>(a)))
237+
Gen.Int.Array().Select(a => (new SetSlim<int>(a), new HashSet<int>(a)))
238238
.SampleModelBased(
239239
Gen.Int.Operation<SetSlim<int>, HashSet<int>>(
240240
(ss, i) => ss.Add(i),
@@ -358,7 +358,7 @@ public void Portfolio_Small_Mixed_Example()
358358
&& p.Positions.Any(p => p.Instrument is Equity)
359359
, "0N0XIzNsQ0O2");
360360
var currencies = portfolio.Positions.Select(p => p.Instrument.Currency).Distinct().ToArray();
361-
var fxRates = ModelGen.Price.Array[currencies.Length].Single(a =>
361+
var fxRates = ModelGen.Price.Array()[currencies.Length].Single(a =>
362362
a.All(p => pp is > 0.75 and < 1.5)
363363
, "ftXKwKhS6ec4");
364364
double fxRate(Currency c) => fxRates[Array.IndexOf(currencies, c)];
@@ -382,7 +382,7 @@ It's just what you need to iteratively improve performance while making sure it
382382
[Test]
383383
public void Faster_Linq_Random()
384384
{
385-
Gen.Byte.Array[100, 1000]
385+
Gen.Byte.Array()[100, 1000]
386386
.Faster(
387387
data => data.Aggregate(0.0, (t, b) => t + b),
388388
data => data.Select(i => (double)i).Sum(),
@@ -412,7 +412,7 @@ Standard Output Messages:
412412
public void Faster_Matrix_Multiply_Range()
413413
{
414414
var genDim = Gen.Int[5, 30];
415-
var genArray = Gen.Double.Unit.Array2D;
415+
var genArray = Gen.Double.Unit.Array2D();
416416
Gen.SelectMany(genDim, genDim, genDim, (i, j, k) => Gen.Select(genArray[i, j], genArray[j, k]))
417417
.Faster(
418418
MulIKJ,
@@ -427,7 +427,7 @@ public void Faster_Matrix_Multiply_Range()
427427
[Test]
428428
public void MapSlim_Performance_Increment()
429429
{
430-
Gen.Byte.Array
430+
Gen.Byte.Array()
431431
.Select(a => (a, new MapSlim<byte, int>(), new Dictionary<int, int>()))
432432
.Faster(
433433
(items, mapslim, _) =>

Tests/AllocatorMany_Tests.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Tests;
1+
namespace Tests;
22

33
using System;
44
using System.Linq;
@@ -10,8 +10,8 @@ public class AllocatorMany_Tests
1010
public void RoundingSolutionTest()
1111
{
1212
Gen.Select(
13-
Gen.Int[1, 1000].Array[2, 200],
14-
Gen.Int[1, 1000].Select(i => (double)i).Array[2, 20])
13+
Gen.Int[1, 1000].Array()[2, 200],
14+
Gen.Int[1, 1000].Select(i => (double)i).Array()[2, 20])
1515
.Sample((rowTotal, colWeight) =>
1616
{
1717
var colTotal = Allocator.Allocate(rowTotal.Sum(), colWeight);
@@ -25,8 +25,8 @@ public void GroupUngroup()
2525
{
2626
Gen.Select(Gen.Int[2, 10], Gen.Int[2, 10]).SelectMany((I, J) =>
2727
Gen.Select(
28-
Gen.Int[0, 5].Array[J].Where(a => a.Sum() > 0).Array[I],
29-
Gen.Int[0, 10].Array[I]))
28+
Gen.Int[0, 5].Array()[J].Where(a => a.Sum() > 0).Array()[I],
29+
Gen.Int[0, 10].Array()[I]))
3030
.Sample((solution,
3131
rowPrice) =>
3232
{
@@ -81,8 +81,8 @@ public async Task AllocatorMany_Classify()
8181
{
8282
Gen.Select(Gen.Int[3, 30], Gen.Int[3, 15]).SelectMany((rows, cols) =>
8383
Gen.Select(
84-
Gen.Int[0, 5].Array[cols].Where(a => a.Sum() > 0).Array[rows],
85-
Gen.Int[900, 1000].Array[rows],
84+
Gen.Int[0, 5].Array()[cols].Where(a => a.Sum() > 0).Array()[rows],
85+
Gen.Int[900, 1000].Array()[rows],
8686
Gen.Int.Uniform))
8787
.Sample((solution,
8888
rowPrice,
@@ -222,9 +222,9 @@ public async Task Example10()
222222
{
223223
Gen.Int[2, 20].SelectMany(rows =>
224224
Gen.Select(
225-
Gen.Int[1, 300].Array[rows],
226-
Gen.Int[1, 100].Array[rows],
227-
Gen.Int[1, 1000].Select(i => (double)i).Array[2, 10],
225+
Gen.Int[1, 300].Array()[rows],
226+
Gen.Int[1, 100].Array()[rows],
227+
Gen.Int[1, 1000].Select(i => (double)i).Array()[2, 10],
228228
Gen.Int.Uniform))
229229
.Sample((rowPrice, rowTotal, weight, seed) =>
230230
{
@@ -274,9 +274,9 @@ public async Task AllocateTest()
274274
{
275275
Gen.Int[2, 20].SelectMany(rows =>
276276
Gen.Select(
277-
Gen.Int[1, 300].Array[rows],
278-
Gen.Int[1, 100].Array[rows],
279-
Gen.Int[1, 1000].Select(i => (double)i).Array[2, 10],
277+
Gen.Int[1, 300].Array()[rows],
278+
Gen.Int[1, 100].Array()[rows],
279+
Gen.Int[1, 1000].Select(i => (double)i).Array()[2, 10],
280280
Gen.Int.Uniform))
281281
.Sample((rowPrice, rowTotal, weight, seed) =>
282282
{

Tests/Allocator_Tests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Tests;
1+
namespace Tests;
22

33
using System;
44
using System.Collections.Generic;
@@ -11,16 +11,16 @@
1111
public class Allocator_Tests
1212
{
1313
readonly static Gen<(long Quantity, double[] Weights)> genAllSigns =
14-
Gen.Select(Gen.Long[-10_000, 10_000], Gen.Double[-10_000, 10_000].Array[2, 50].Where(ws => ws.Sum() > 1e-9));
14+
Gen.Select(Gen.Long[-10_000, 10_000], Gen.Double[-10_000, 10_000].Array()[2, 50].Where(ws => ws.Sum() > 1e-9));
1515

1616
readonly static Gen<(long Quantity, long[] Weights)> genAllSignsLong =
17-
Gen.Select(Gen.Long[-10_000, 10_000], Gen.Long[-100_000, 100_000].Array[2, 50].Where(ws => ws.Sum() != 0));
17+
Gen.Select(Gen.Long[-10_000, 10_000], Gen.Long[-100_000, 100_000].Array()[2, 50].Where(ws => ws.Sum() != 0));
1818

1919
readonly static Gen<(long Quantity, double[] Weights)> genPositive =
20-
Gen.Select(Gen.Long[1, 10_000], Gen.Double[0, 10_000].Array[2, 50].Where(ws => Math.Abs(ws.Sum()) > 1e-9));
20+
Gen.Select(Gen.Long[1, 10_000], Gen.Double[0, 10_000].Array()[2, 50].Where(ws => Math.Abs(ws.Sum()) > 1e-9));
2121

2222
readonly static Gen<(long Quantity, long[] Weights)> genPositiveLong =
23-
Gen.Select(Gen.Long[1, 10_000], Gen.Long[0, 100_000].Array[2, 50].Where(ws => ws.Sum() != 0));
23+
Gen.Select(Gen.Long[1, 10_000], Gen.Long[0, 100_000].Array()[2, 50].Where(ws => ws.Sum() != 0));
2424

2525
static bool TotalCorrectly<W>(long quantity, W[] weights, Func<long, W[], long[]> allocator)
2626
=> allocator(quantity, weights).Sum() == quantity;
@@ -276,7 +276,7 @@ public void Allocate_HasSmallestAllocationError()
276276
var genInt = Gen.Int[0, i - 1];
277277
return Gen.Select(genInt, genInt)
278278
.Where((i, j) => i != j)
279-
.HashSet[1, i];
279+
.HashSet()[1, i];
280280
}
281281
genAllSigns.Where((_, ws) => ws.Length >= 2)
282282
.SelectMany((quantity, weights) => GenChanges(weights.Length).Select(i => (quantity, weights, i)))
@@ -318,7 +318,7 @@ public void Allocate_Long_HasSmallestAllocationError()
318318
var genInt = Gen.Int[0, i - 1];
319319
return Gen.Select(genInt, genInt)
320320
.Where((i, j) => i != j)
321-
.HashSet[1, i];
321+
.HashSet()[1, i];
322322
}
323323
genAllSignsLong.Where((_, ws) => ws.Length >= 2)
324324
.SelectMany((quantity, weights) => GenChanges(weights.Length).Select(i => (quantity, weights, i)))

Tests/CacheTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Tests;
1+
namespace Tests;
22

33
using CsCheck;
44
using System.Collections.Concurrent;
@@ -11,7 +11,7 @@ public void Cache_GetOrAdd_ModelBased()
1111
static void CacheTryAdd(Cache<int, byte> cache, int key, byte value)
1212
=> cache.GetOrAdd(key, _ => Task.FromResult(value)).AsTask().GetAwaiter().GetResult();
1313

14-
Gen.Select(Gen.Int, Gen.Byte).Array
14+
Gen.Select(Gen.Int, Gen.Byte).Array()
1515
.Select(kvs =>
1616
{
1717
var cache = new Cache<int, byte>();
@@ -33,7 +33,7 @@ static void CacheTryAdd(Cache<int, byte> cache, int key, byte value)
3333
[Test]
3434
public async Task Cache_GetOrAdd_StampedeFree()
3535
{
36-
await Gen.Int.HashSet[1, 10].SampleAsync(async ks =>
36+
await Gen.Int.HashSet()[1, 10].SampleAsync(async ks =>
3737
{
3838
var alreadyRun = 0;
3939
var ks0 = ks.First();
@@ -147,7 +147,7 @@ async Task<int> Factory(int _)
147147
[Test]
148148
public async Task Cache_Update_StampedeFree()
149149
{
150-
await Gen.Int.HashSet[1, 10].SampleAsync(async ks =>
150+
await Gen.Int.HashSet()[1, 10].SampleAsync(async ks =>
151151
{
152152
var alreadyRun = 0;
153153
var callCount = 0;

0 commit comments

Comments
 (0)