Skip to content

Commit 9d23026

Browse files
committed
type members of ad hoc union can be flagged as "stateless"
1 parent 0b89f31 commit 9d23026

59 files changed

Lines changed: 7675 additions & 95 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE-ATTRIBUTES.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,11 @@ Ad-hoc unions for 2-5 types (generic syntax).
179179

180180
**Properties** (per generic parameter):
181181

182-
| Property | Type | Default | Description |
183-
|---------------------------------------------------------------|-----------|-----------|--------------------------------------------------------------------------|
184-
| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. |
185-
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ... | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) |
182+
| Property | Type | Default | Description |
183+
|---------------------------------------------------------------|-----------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
184+
| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. |
185+
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ... | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types). Automatically set to `true` for reference types when `TXIsStateless = true` |
186+
| `T1IsStateless`, `T2IsStateless`, ... | `bool` | `false` | Mark T1, T2, etc. as a stateless type that carries no instance data. Reduces memory by storing only discriminator index. Accessors return `default(T)`. Automatically sets `TXIsNullableReferenceType = true` for reference types. **Recommended: Use struct types for stateless members to avoid null-handling complexity.** |
186187

187188
**Inherits all properties from `UnionAttributeBase`** (see above).
188189

@@ -202,12 +203,13 @@ AdHocUnionAttribute(Type t1, Type t2, Type? t3 = null, Type? t4 = null, Type? t5
202203

203204
**Properties**:
204205

205-
| Property | Type | Default | Description |
206-
|--------------------------------------------------------------------------------------------|-----------|------------|--------------------------------------------------------------------------|
207-
| `T1`, `T2` | `Type` | (required) | Required member types |
208-
| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types |
209-
| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. |
210-
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ..., `T5IsNullableReferenceType` | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) |
206+
| Property | Type | Default | Description |
207+
|--------------------------------------------------------------------------------------------|-----------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
208+
| `T1`, `T2` | `Type` | (required) | Required member types |
209+
| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types |
210+
| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. |
211+
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ..., `T5IsNullableReferenceType` | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types). Automatically set to `true` for reference types when `TXIsStateless = true` |
212+
| `T1IsStateless`, `T2IsStateless`, ..., `T5IsStateless` | `bool` | `false` | Mark T1, T2, etc. as a stateless type that carries no instance data. Reduces memory by storing only discriminator index. Accessors return `default(T)`. Automatically sets `TXIsNullableReferenceType = true` for reference types. **Recommended: Use struct types for stateless members to avoid null-handling complexity.** |
211213

212214
**Inherits all properties from `UnionAttributeBase`** (see above).
213215

.claude/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ This is a .NET library providing **Smart Enums**, **Value Objects**, and **Discr
151151

152152
- **Ad-hoc `[Union<T1, T2>]` or `[AdHocUnion(typeof(T1), typeof(T2))]`**: Simple 2-5 type combinations
153153
- Implicit conversion operators, IsT1/AsT1 properties, Switch/Map
154+
- Stateless types (`TXIsStateless = true`): Memory-efficient members that store only discriminator, not instance data (prefer structs to avoid null-handling)
154155
- **Regular `[Union]`**: Inheritance-based unions with derived types
155156
- Static factory methods, Switch/Map over all derived types
156157

.serena/project.yml

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
2-
# * For C, use cpp
3-
# * For JavaScript, use typescript
4-
# Special requirements:
5-
# * csharp: Requires the presence of a .sln file in the project folder.
6-
language: csharp
71

82
# whether to use the project's gitignore file to ignore files
93
# Added on 2025-04-07
@@ -64,5 +58,58 @@ excluded_tools: []
6458
# initial prompt for the project. It will always be given to the LLM upon activating the project
6559
# (contrary to the memories, which are loaded on demand).
6660
initial_prompt: ""
67-
61+
# the name by which the project can be referenced within Serena
6862
project_name: "Thinktecture.Runtime.Extensions"
63+
64+
# list of mode names to that are always to be included in the set of active modes
65+
# The full set of modes to be activated is base_modes + default_modes.
66+
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
67+
# Otherwise, this setting overrides the global configuration.
68+
# Set this to [] to disable base modes for this project.
69+
# Set this to a list of mode names to always include the respective modes for this project.
70+
base_modes:
71+
72+
# list of mode names that are to be activated by default.
73+
# The full set of modes to be activated is base_modes + default_modes.
74+
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
75+
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
76+
# This setting can, in turn, be overridden by CLI parameters (--mode).
77+
default_modes:
78+
79+
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
80+
included_optional_tools: []
81+
82+
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
83+
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
84+
fixed_tools: []
85+
86+
# the encoding used by text files in the project
87+
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
88+
encoding: utf-8
89+
90+
91+
# list of languages for which language servers are started; choose from:
92+
# al bash clojure cpp csharp
93+
# csharp_omnisharp dart elixir elm erlang
94+
# fortran fsharp go groovy haskell
95+
# java julia kotlin lua markdown
96+
# matlab nix pascal perl php
97+
# powershell python python_jedi r rego
98+
# ruby ruby_solargraph rust scala swift
99+
# terraform toml typescript typescript_vts vue
100+
# yaml zig
101+
# (This list may be outdated. For the current list, see values of Language enum here:
102+
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
103+
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
104+
# Note:
105+
# - For C, use cpp
106+
# - For JavaScript, use typescript
107+
# - For Free Pascal/Lazarus, use pascal
108+
# Special requirements:
109+
# Some languages require additional setup/installations.
110+
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
111+
# When using multiple languages, the first language server that supports a given file will be used for that file.
112+
# The first language is the default language and the respective language server will be used as a fallback.
113+
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
114+
languages:
115+
- csharp

docs

Submodule docs updated from 8e82408 to b83366b
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Thinktecture.Unions;
2+
3+
/// <summary>
4+
/// API response union demonstrating stateless types for memory optimization.
5+
/// NotFound and Unauthorized are stateless types that carry no instance data.
6+
/// </summary>
7+
[Union<SuccessResponse, NotFound, Unauthorized>(
8+
T1Name = "Success",
9+
T2Name = "NotFound", T2IsStateless = true,
10+
T3Name = "Unauthorized", T3IsStateless = true)]
11+
public partial class ApiResponseWithStateless;
12+
13+
public sealed class SuccessResponse
14+
{
15+
public required string Data { get; init; }
16+
}
17+
18+
/// <summary>
19+
/// Stateless type for "not found" state.
20+
/// No backing field is allocated - only the discriminator index is stored.
21+
/// Using a struct avoids null-handling complexity since default(NotFound) is a valid value.
22+
/// </summary>
23+
public readonly record struct NotFound;
24+
25+
/// <summary>
26+
/// Stateless type for "unauthorized" state.
27+
/// No backing field is allocated - only the discriminator index is stored.
28+
/// Using a struct avoids null-handling complexity since default(Unauthorized) is a valid value.
29+
/// </summary>
30+
public readonly record struct Unauthorized;

samples/Basic.Samples/Unions/DiscriminatedUnionsDemos.cs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public static void Demo(ILogger logger)
1515
DemoForJurisdiction(logger);
1616
DemoForPartiallyKnownDate(logger);
1717
DemoForNestedUnionsAndSwitchMapOverloads(logger);
18+
DemoForStatelessTypes(logger);
1819
}
1920

2021
private static void DemoForAdHocUnions(ILogger logger)
@@ -336,8 +337,8 @@ void HandleFailure(ApiResponse.Failure failure)
336337
// With simple parameter naming, nested types use only their own name
337338
apiResponseSimple.Switch(
338339
success: success => logger.Information("[Switch] Success"),
339-
notFound: notFound => logger.Information("[Switch] Not Found"), // Simple name
340-
unauthorized: unauthorized => logger.Information("[Switch] Unauthorized") // Simple name
340+
notFound: notFound => logger.Information("[Switch] Not Found"), // Simple name
341+
unauthorized: unauthorized => logger.Information("[Switch] Unauthorized") // Simple name
341342
);
342343

343344
// Non-exhaustive overload (stopped at Failure level)
@@ -353,4 +354,89 @@ void HandleFailureSimple(ApiResponseWithSimpleParameterNames.Failure failure)
353354
);
354355
}
355356
}
357+
358+
private static void DemoForStatelessTypes(ILogger logger)
359+
{
360+
logger.Information("""
361+
362+
363+
==== Demo for Stateless Types (Memory Optimization) ====
364+
365+
""");
366+
367+
// Creating different API responses
368+
ApiResponseWithStateless successResponse = new SuccessResponse { Data = "Hello, World!" };
369+
ApiResponseWithStateless notFoundResponse = new NotFound();
370+
ApiResponseWithStateless unauthorizedResponse = new Unauthorized();
371+
372+
logger.Information("--- Type Checking ---");
373+
logger.Information("Success response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
374+
successResponse.IsSuccess, successResponse.IsNotFound, successResponse.IsUnauthorized);
375+
logger.Information("NotFound response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
376+
notFoundResponse.IsSuccess, notFoundResponse.IsNotFound, notFoundResponse.IsUnauthorized);
377+
logger.Information("Unauthorized response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
378+
unauthorizedResponse.IsSuccess, unauthorizedResponse.IsNotFound, unauthorizedResponse.IsUnauthorized);
379+
380+
logger.Information("--- Accessing Values ---");
381+
// For stateless types, accessors return default(T)
382+
var notFoundValue = notFoundResponse.AsNotFound;
383+
logger.Information("NotFound accessor returns: {Value} (default struct)", notFoundValue);
384+
385+
var unauthorizedValue = unauthorizedResponse.AsUnauthorized;
386+
logger.Information("Unauthorized accessor returns: {Value} (default struct)", unauthorizedValue);
387+
388+
// Regular member still returns the actual instance
389+
var successValue = successResponse.AsSuccess;
390+
logger.Information("Success accessor returns: {Data}", successValue.Data);
391+
392+
logger.Information("--- Switch Pattern Matching ---");
393+
successResponse.Switch(
394+
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
395+
notFound: _ => logger.Information("[Switch] Not Found"),
396+
unauthorized: _ => logger.Information("[Switch] Unauthorized")
397+
);
398+
399+
notFoundResponse.Switch(
400+
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
401+
notFound: _ => logger.Information("[Switch] Not Found"),
402+
unauthorized: _ => logger.Information("[Switch] Unauthorized")
403+
);
404+
405+
unauthorizedResponse.Switch(
406+
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
407+
notFound: _ => logger.Information("[Switch] Not Found"),
408+
unauthorized: _ => logger.Information("[Switch] Unauthorized")
409+
);
410+
411+
logger.Information("--- Map Pattern Matching ---");
412+
var statusCode = successResponse.Map(
413+
success: 200,
414+
notFound: 404,
415+
unauthorized: 401
416+
);
417+
logger.Information("Success response maps to status code: {StatusCode}", statusCode);
418+
419+
statusCode = notFoundResponse.Map(
420+
success: 200,
421+
notFound: 404,
422+
unauthorized: 401
423+
);
424+
logger.Information("NotFound response maps to status code: {StatusCode}", statusCode);
425+
426+
statusCode = unauthorizedResponse.Map(
427+
success: 200,
428+
notFound: 404,
429+
unauthorized: 401
430+
);
431+
logger.Information("Unauthorized response maps to status code: {StatusCode}", statusCode);
432+
433+
logger.Information("--- Equality Comparison ---");
434+
var anotherNotFound = new NotFound();
435+
logger.Information("notFoundResponse == anotherNotFound: {IsEqual}", notFoundResponse == anotherNotFound);
436+
437+
var anotherUnauthorized = new Unauthorized();
438+
logger.Information("unauthorizedResponse == anotherUnauthorized: {IsEqual}", unauthorizedResponse == anotherUnauthorized);
439+
440+
logger.Information("notFoundResponse == unauthorizedResponse: {IsEqual}", notFoundResponse == unauthorizedResponse);
441+
}
356442
}

0 commit comments

Comments
 (0)