-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
Add a source generator that produces a complete implementation of the Memento pattern for consumer types — including classes, structs, record classes, and record structs — focused on:
- capturing an immutable snapshot (“memento”)
- restoring that snapshot
- optionally generating a caretaker for undo/redo history
- providing safe-by-default capture strategies for mutable members
- staying deterministic, reflection-free, and consistent with PatternKit’s “patterns without boilerplate” direction
Motivation / Problem
Memento implementations are repetitive and correctness-sensitive:
- picking which members to include/exclude
- avoiding aliasing (mutable references captured by reference)
- immutability guarantees for snapshots
- versioning when the originator evolves
- undo/redo history stacks, capacity, redo invalidation
- supporting immutable originators (records) without awkward mutation paths
This is ideal for source generation: emit correct, optimized code once, consistently.
Supported Originator Kinds (must-have)
The generator must support:
classstructrecord classrecord struct
Each kind has different restore semantics:
Mutable originators (class/struct with settable members)
- restore can be “in-place” (assign members back)
Immutable originators (records with init or positional parameters)
-
restore should support returning a new instance:
RestoreNew(...)orApply(...)
-
optionally support
with-expression restore when feasible:- if the generator can express a valid
with { ... }for the included members
- if the generator can express a valid
We should not force mutation on records that are designed to be immutable.
Proposed User Experience
Minimal: snapshot only
[Memento]
public partial record class EditorState(string Text, int Cursor);Generated (shape):
public readonly partial struct EditorStateMemento
{
public string Text { get; }
public int Cursor { get; }
public static EditorStateMemento Capture(in EditorState originator);
// Immutable-friendly restore
public EditorState RestoreNew();
// Optional: only generated if the originator is mutable
public void Restore(EditorState originator);
}Snapshot + caretaker (undo/redo)
[Memento(GenerateCaretaker = true, Capacity = 256)]
public partial record class EditorState(string Text, int Cursor);Generated:
public sealed partial class EditorStateHistory
{
public int Count { get; }
public bool CanUndo { get; }
public bool CanRedo { get; }
public EditorState Current { get; }
public EditorStateHistory(EditorState initial);
public void Capture(EditorState state);
public bool Undo();
public bool Redo();
public void Clear();
}Notes
- For immutable originators, caretaker stores states, not “a reference that gets mutated.”
- For mutable originators, caretaker can store mementos and restore into the same instance (optional; see “Caretaker modes”).
Member Selection
Support both modes:
Include-all (default, configurable)
- include all eligible public instance fields/properties with getters
- exclude explicitly ignored members
Include-explicit
- include only
[MementoInclude]
Attributes:
[Memento][MementoIgnore][MementoInclude]
Capture Strategies for Mutable References (footgun prevention)
Default strategy should be explicit and safe:
- value types copied
stringtreated as safe immutable- reference types default to
ByReferencewith a warning unless configured otherwise
Per-member strategy attribute:
[MementoStrategy(Clone)]
public List<Item> Items { get; init; } = new();Strategies:
ByReferenceClone(requires a known mechanism)DeepCopy(only when generator can safely emit it)Serialize(opt-in, future)Custom(partial hook)
Restore Semantics (by originator type)
For class / struct (mutable)
Generate both:
void Restore(TOriginator originator)(in-place)TOriginator RestoreNew()(convenience)
For record class / record struct
Generate:
TOriginator RestoreNew()(always)- optional
Apply(TOriginator originator)only if the record has writable members (rare / discouraged)
Additionally:
-
if feasible, prefer
with-expression:originator with { Prop = value, ... }
-
otherwise, use the best available constructor/positional parameters:
- either primary constructor args or a synthesized factory based on accessible constructors
The generator must emit diagnostics if it cannot safely construct a new record instance.
Caretaker Generation (Undo/Redo)
Caretaker should be optional and support deterministic behavior:
Two caretaker modes (choose via config)
-
State-based caretaker (recommended default)
- stores
TOriginatorinstances (works for all originator kinds, especially records) Capture(state)pushes a new stateUndo()/Redo()swaps current state
- stores
-
Memento-based caretaker (optional, for mutable classes)
- stores mementos and restores into a live originator reference
Capacity:
- fixed ring buffer (default when capacity set)
- deterministic eviction (drop oldest)
Redo invalidation:
- after
Undo(), callingCapture(...)clears redo stack
Versioning & Compatibility
Generated memento should include version metadata:
-
int Versionor deterministic hash of included members -
restore behavior:
- default: hard-fail on mismatch
- optional: best-effort restore matching members only
Diagnostics (must-have)
Stable diagnostic IDs, actionable messages:
PKMEM001Type marked[Memento]must be partial (when injecting members into the type)PKMEM002Member inaccessible for capture/restorePKMEM003Unsafe reference capture (warning): mutable reference captured by referencePKMEM004Clone strategy requested but clone mechanism missingPKMEM005Record restore generation failed (no accessible constructor / with-path not viable)PKMEM006Init-only/setter restrictions prevent restore-in-place (info/warn, and generator falls back to RestoreNew)
Generated Code Layout
TypeName.Memento.g.csTypeName.History.g.cs(if caretaker enabled)
Deterministic ordering and stable naming.
Testing Expectations
Add tests verifying:
Records
- record class capture/restore-new works
- record struct capture/restore-new works
with-path used when possible (or at least behavior matches)- diagnostics appear when restore-new is not constructible
Mutable types
- restore-in-place works
- member inclusion/exclusion works
- reference strategies behave as configured
Caretaker
- capture → undo → redo (records + classes)
- redo invalidation after capture
- capacity eviction deterministic
Acceptance Criteria
-
[Memento]works for class/struct/record class/record struct - Generated memento is immutable and supports capture/restore
- For records, generator emits RestoreNew() as the primary restore mechanism
- Optional caretaker supports undo/redo with deterministic semantics (state-based works everywhere)
- Configurable member selection and capture strategies
- Diagnostics cover the common footguns and record construction constraints
- Tests cover records + caretaker behaviors