-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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
Visitmethod for each concrete element - an
Acceptmethod 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:
IShapeVisitorinterface withVisit(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_ValidationIAstNodeVisitor_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
Acceptgenerated. - 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.