Skip to content

Generator: Create Memento Pattern (supports class/struct/record + optional undo/redo caretaker) #44

@JerrettDavis

Description

@JerrettDavis

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:

  1. class
  2. struct
  3. record class
  4. record 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(...) or Apply(...)
  • optionally support with-expression restore when feasible:

    • if the generator can express a valid with { ... } for the included members

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
  • string treated as safe immutable
  • reference types default to ByReference with a warning unless configured otherwise

Per-member strategy attribute:

[MementoStrategy(Clone)]
public List<Item> Items { get; init; } = new();

Strategies:

  • ByReference
  • Clone (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)

  1. State-based caretaker (recommended default)

    • stores TOriginator instances (works for all originator kinds, especially records)
    • Capture(state) pushes a new state
    • Undo()/Redo() swaps current state
  2. 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(), calling Capture(...) clears redo stack

Versioning & Compatibility

Generated memento should include version metadata:

  • int Version or 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:

  • PKMEM001 Type marked [Memento] must be partial (when injecting members into the type)
  • PKMEM002 Member inaccessible for capture/restore
  • PKMEM003 Unsafe reference capture (warning): mutable reference captured by reference
  • PKMEM004 Clone strategy requested but clone mechanism missing
  • PKMEM005 Record restore generation failed (no accessible constructor / with-path not viable)
  • PKMEM006 Init-only/setter restrictions prevent restore-in-place (info/warn, and generator falls back to RestoreNew)

Generated Code Layout

  • TypeName.Memento.g.cs
  • TypeName.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions