Skip to content

Generator: Create Visitor Pattern #48

@JerrettDavis

Description

@JerrettDavis

Summary

Implement a source generator that emits all the boilerplate normally required for the Visitor design pattern, including:

  • visitor interface definitions
  • accept methods on element types
  • visitor dispatch logic
  • optional generic return types for visitors
  • support for multiple visitor families
  • ergonomic annotations to opt in and control behavior

All generated code should be:

  • reflection-free
  • compile-time deterministic
  • allocation-light
  • consistent with PatternKit’s source-gen approach

Motivation & Context

The Visitor pattern is a classic behavioral design pattern that separates algorithms from the objects on which they operate, enabling new operations to be added without modifying the object structure. ([refactoring.guru][2])

In C#, implementing a visitor typically requires:

  • a visitor interface with a Visit method for each concrete element
  • an Accept method in every element
  • a lot of repetitive boilerplate
  • error-prone pattern matching or casts

Source generation can make this automatic, safe, and ergonomic.


Goals

G1 — Source-gen visitor interfaces and accept methods

From minimal input, generate a visitor interface (or interfaces) and implement the required accept methods on all participating types.

G2 — Support multiple visitor families

Allow multiple independent visitor sets within a single project.

For example, one family for validation logic and another for serialization logic.

G3 — Support void and generic visitors

Visitor operations should optionally return values (TResult) and accept context parameters.

G4 — Make the generated API ergonomic

Prefer compile-time dispatch over switch / if / type casting. Avoid reflection entirely.

G5 — Deterministic ordering & diagnostics

The generator should provide diagnostics if new concrete types are introduced but not included in visitors, and deterministic visit order where applicable.


User Experience

Opt-in Model

Users mark a base type and, optionally, concrete types they want included in a visitor family.

Example: Interface base

[VisitorNode]
public partial interface IShape { }

User defines shapes:

public partial class Circle : IShape { /*...*/ }
public partial class Rectangle : IShape { /*...*/ }
public partial class Triangle : IShape { /*...*/ }

The generator produces:

  • IShapeVisitor interface with Visit(Circle), Visit(Rectangle), Visit(Triangle)
  • Accept(visitor) on each concrete type

Example: Abstract base class

[VisitorNode]
public abstract partial class Node { }

User defines:

public partial class Leaf : Node { /* ... */ }
public partial class Branch : Node { /* ... */ }

Generated code augments both with Accept.


Generated Output (Desired Shape)

Visitor Interface

Generated on the base type:

public interface IShapeVisitor
{
    void Visit(Circle shape);
    void Visit(Rectangle shape);
    void Visit(Triangle shape);
}

Generic Visitor

Optionally support returning a result:

public interface IShapeVisitor<TResult>
{
    TResult Visit(Circle shape);
    TResult Visit(Rectangle shape);
    TResult Visit(Triangle shape);
}

Accept Methods

public partial class Circle : IShape
{
    public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
    public TResult Accept<TResult>(IShapeVisitor<TResult> visitor)
        => visitor.Visit(this);
}

Multiple Visitor Families Supported

Example with diagnostics:

[VisitorNode("Validation")]
public partial interface IAstNode { }

[VisitorNode("Rendering")]
public partial interface IAstNode { }

Each family emits:

  • IAstNodeVisitor_Validation
  • IAstNodeVisitor_Rendering
  • augment accept methods accordingly

Options & Extensions

Support for Interface Segregation

Allow optional base visitor interfaces to group common visit methods:

[VisitorNode(BaseVisitor = typeof(IVisitorBase))]
public partial interface IExpr { }

Generated visitors implement the base.

Context Parameters

Visitors may accept a context parameter:

interface IShapeVisitor<TContext>
{
    void Visit(Circle shape, TContext ctx);}

This allows stateful visitors without external state capture.

Default Implementation Hooks

Generated partial visitor interface stub could contain no-op defaults:

public partial interface IShapeVisitor
{
    void Visit(Circle shape) => DefaultVisit(shape);}

Optional generator flag to emit defaults.


Diagnostics

Provide generator diagnostics for:

ID Meaning
PKVIS001 No concrete types found for a marked base type
PKVIS002 Partial accept method not implementable due to accessibility
PKVIS003 Visitor family missing a visit method for a discovered concrete type
PKVIS004 Duplicate visitor families with conflicting names

Diagnostics should point at base type and list missing concrete types.


Edge Cases & Rules

Partial Requirements

Originating base types must be marked partial if accept methods are injected into them.

Concrete types must be partial when generator needs to emit additional members.

Abstract Types

Include abstract classes as part of the hierarchy, but only emit Visit for concrete implementors.

Generic Types

Support type parameters on base and concrete types.

Generated visitors must mirror generic shape.

Value Types

Support struct nodes; calls remain allocation-free.


Tests & Coverage

Test cases should cover:

  • interface base, abstract base, mixed hierarchies
  • visitors with return types
  • multiple families
  • generic element types
  • context parameters
  • missing implementations → generator diagnostics
  • ordering stability between builds

Acceptance Criteria

  • Marking a base type with [VisitorNode] generates a visitor family and accept methods.
  • Concrete types in the hierarchy all get Accept generated.
  • Supports both void and generic result visitors.
  • Optional context visitor variants compile with no reflection overhead.
  • Multiple visitor families per base type supported.
  • Clear diagnostics when a visitor is missing a handler or a type is inaccessible.
  • Generated code is deterministic, minimal, and avoids runtime reflection.

Example Complete Flow

Before generator:

[VisitorNode]
public partial interface IAstNode { }

public partial class NumberNode : IAstNode {}
public partial class BinaryOpNode : IAstNode {}

After generator:

public interface IAstNodeVisitor
{
    void Visit(NumberNode node);
    void Visit(BinaryOpNode node);
}

public partial class NumberNode
{
    public void Accept(IAstNodeVisitor v) => v.Visit(this);
}

public partial class BinaryOpNode
{
    public void Accept(IAstNodeVisitor v) => v.Visit(this);
}

Clients then write visitors cleanly, relying on static dispatch rather than conditionals and without boilerplate.

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions