Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**Chain**](chain.md) | Chain-of-responsibility pipelines | `[Chain]` |
| [**Command**](command.md) | Command objects and invokers | `[Command]` |
| [**Composer**](composer.md) | Pipeline composition from ordered steps | `[Composer]` |
| [**Interpreter**](interpreter.md) | DSL rule factories for terminal and non-terminal expressions | `[GenerateInterpreter]` |
| [**Iterator**](iterator.md) | Enumerable/async-enumerable iteration helpers | `[Iterator]` |
| [**Memento**](memento.md) | Immutable snapshots with optional undo/redo history | `[Memento]` |
| [**Observer**](observer.md) | Event hubs and observer dispatch | `[ObserverHub]` |
Expand Down Expand Up @@ -134,6 +135,10 @@ public partial class EditorState { }
[GenerateStrategy("Router", typeof(Request), StrategyKind.Action)]
public partial class Router { }

// Interpreter - generated DSL rule factory
[GenerateInterpreter(typeof(PricingContext), typeof(decimal))]
public static partial class PricingRules { }

// Template Method - algorithm skeleton
[Template]
public abstract partial class DataProcessor { }
Expand Down
93 changes: 93 additions & 0 deletions docs/generators/interpreter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Interpreter Generator

The Interpreter generator turns annotated rule methods into an immutable `Interpreter<TContext, TResult>` factory. Use it when a DSL grammar is stable enough to describe in code and you want compile-time diagnostics, deterministic registration order, and a factory that can be imported through `IServiceCollection`.

## Quick Start

```csharp
using PatternKit.Generators.Interpreter;

[GenerateInterpreter(typeof(PricingContext), typeof(decimal), FactoryMethodName = "Build")]
public static partial class PricingRules
{
[InterpreterTerminal("number")]
private static decimal Number(string token) => decimal.Parse(token);

[InterpreterTerminal("cart_total")]
private static decimal CartTotal(string token, PricingContext context) => context.CartTotal;

[InterpreterNonTerminal("add")]
private static decimal Add(decimal[] args) => args[0] + args[1];

[InterpreterNonTerminal("round")]
private static decimal Round(decimal[] args) => Math.Round(args[0], 2);
}

var interpreter = PricingRules.Build();
```

The generated method composes the existing fluent runtime:

```csharp
var builder = Interpreter.Create<PricingContext, decimal>();
builder.Terminal("number", static (token, context) => Number(token));
builder.Terminal("cart_total", static (token, context) => CartTotal(token, context));
builder.NonTerminal("add", static (args, context) => Add(args));
builder.NonTerminal("round", static (args, context) => Round(args));
return builder.Build();
```

## Attribute Model

| Attribute | Target | Purpose |
| --- | --- | --- |
| `[GenerateInterpreter(typeof(TContext), typeof(TResult))]` | partial class or struct | Generates an `Interpreter<TContext, TResult>` factory. |
| `[InterpreterTerminal("name")]` | static method | Registers a terminal handler for token values. |
| `[InterpreterNonTerminal("name")]` | static method | Registers a non-terminal handler for child results. |

## Valid Rule Signatures

Terminal handlers return `TResult` and accept either:

```csharp
static TResult Rule(string token)
static TResult Rule(string token, TContext context)
```

Non-terminal handlers return `TResult` and accept either:

```csharp
static TResult Rule(TResult[] args)
static TResult Rule(TResult[] args, TContext context)
```

Rules can be private because the generated factory is emitted into the same partial type.

## Diagnostics

| Id | Meaning |
| --- | --- |
| `PKINT001` | The host type is marked with `[GenerateInterpreter]` but is not partial. |
| `PKINT002` | The host has no terminal or non-terminal rules. |
| `PKINT003` | A rule method is not static, returns the wrong type, or has an invalid parameter list. |
| `PKINT004` | A terminal or non-terminal rule name is registered more than once. |

## IServiceCollection Integration

Register the generated interpreter as a singleton. The interpreter is immutable after build and can be safely shared by request handlers, hosted services, ASP.NET Core endpoints, and background workers.

```csharp
public static IServiceCollection AddPricingRules(this IServiceCollection services)
{
services.AddSingleton(_ => PricingRules.Build());
return services;
}
```

PatternKit's e-commerce Interpreter example exposes both fluent and generated pricing/eligibility interpreters and registers the generated path through `AddPatternKitExamples`.

## See Also

- [Interpreter Pattern](../patterns/behavioral/interpreter/index.md)
- [Interpreter Real-World Examples](../patterns/behavioral/interpreter/real-world-examples.md)
- [Pattern Coverage Guide](../guides/pattern-coverage.md)
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
- name: Flyweight
href: flyweight.md

- name: Interpreter
href: interpreter.md

- name: Iterator
href: iterator.md

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/pattern-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Structural | Proxy | `Proxy<TIn, TOut>` | Proxy generator |
| Behavioral | Chain of Responsibility | `ActionChain<T>` and `ResultChain<T>` | Chain generator |
| Behavioral | Command | `Command<T>` | Command generator |
| Behavioral | Interpreter | `Interpreter<TContext, TResult>` | Tracked in [#206](https://github.com/JerrettDavis/PatternKit/issues/206) |
| Behavioral | Interpreter | `Interpreter<TContext, TResult>` | Interpreter generator |
| Behavioral | Iterator | `Flow<T>` and sequence helpers | Iterator generator |
| Behavioral | Mediator | `Mediator` | Dispatcher generator |
| Behavioral | Memento | `Memento<T>` | Memento generator |
Expand Down
24 changes: 24 additions & 0 deletions docs/patterns/behavioral/interpreter/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ PatternKit's implementation uses a fluent builder API to define:

The interpreter recursively evaluates the expression tree, producing a final result.

PatternKit also includes a source-generated path for stable grammars. Annotate a partial rule host with `[GenerateInterpreter]`, mark static methods with `[InterpreterTerminal]` and `[InterpreterNonTerminal]`, and the generator emits an immutable interpreter factory that composes the same runtime without reflection.

## When to Use

- **Domain-specific languages (DSLs)**: Building custom languages for configuration, rules, or queries
Expand All @@ -58,6 +60,27 @@ The interpreter recursively evaluates the expression tree, producing a final res
| `AsyncInterpreter<TContext, TResult>` | Async interpreter returning a value | External lookups, I/O |
| `AsyncActionInterpreter<TContext>` | Async interpreter with side effects | Async commands |

## Generated Path

```csharp
[GenerateInterpreter(typeof(PricingContext), typeof(decimal), FactoryMethodName = "Build")]
public static partial class PricingRules
{
[InterpreterTerminal("number")]
private static decimal Number(string token) => decimal.Parse(token);

[InterpreterTerminal("cart_total")]
private static decimal CartTotal(string token, PricingContext context) => context.CartTotal;

[InterpreterNonTerminal("add")]
private static decimal Add(decimal[] args) => args[0] + args[1];
}

var interpreter = PricingRules.Build();
```

Use the fluent builder when a grammar is assembled dynamically. Use the generated path when the grammar belongs to the application contract and should be validated at compile time.

## Diagram

```mermaid
Expand Down Expand Up @@ -97,5 +120,6 @@ classDiagram
- [Comprehensive Guide](guide.md) - Detailed usage and patterns
- [API Reference](api-reference.md) - Complete API documentation
- [Real-World Examples](real-world-examples.md) - Production-ready examples
- [Interpreter Generator](../../../generators/interpreter.md) - Source-generated rule factories
- [Strategy Pattern](../strategy/index.md) - For simple conditional logic
- [Composite Pattern](../../structural/composite/index.md) - For tree structures without interpretation
28 changes: 28 additions & 0 deletions docs/patterns/behavioral/interpreter/real-world-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,34 @@ var tierDiscount = interpreter.Interpret(tierDiscountRule, context); // 15.00
var thresholdDiscount = interpreter.Interpret(thresholdRule, context); // 5.00
```

### Source-Generated Variant

For production applications with a stable pricing grammar, the same rule set can be declared with generator attributes and registered through dependency injection:

```csharp
[GenerateInterpreter(typeof(PricingContext), typeof(decimal), FactoryMethodName = "Create")]
public static partial class GeneratedPricingRuleInterpreter
{
[InterpreterTerminal("number")]
private static decimal Number(string token) => decimal.Parse(token);

[InterpreterTerminal("var")]
private static decimal Variable(string token, PricingContext context) =>
token == "cart_total" ? context.CartTotal : 0m;

[InterpreterNonTerminal("mul")]
private static decimal Multiply(decimal[] args) => args[0] * args[1];
}

public static IServiceCollection AddPricingRules(this IServiceCollection services)
{
services.AddSingleton(_ => GeneratedPricingRuleInterpreter.Create());
return services;
}
```

The full `InterpreterDemo` includes generated pricing and eligibility interpreters that are validated by TinyBDD tests and imported through `AddPatternKitExamples`.

### Why This Pattern

- **Business agility**: Rules can be stored in a database and changed without deployment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using PatternKit.Behavioral.Chain;
using PatternKit.Behavioral.Interpreter;
using PatternKit.Behavioral.Strategy;
using PatternKit.Behavioral.TypeDispatcher;
using PatternKit.Creational.AbstractFactory;
Expand Down Expand Up @@ -44,6 +45,7 @@
using TransactionPipeline = PatternKit.Examples.Chain.TransactionPipeline;
using VisitorTender = PatternKit.Examples.VisitorDemo.Tender;
using WidgetDemo = PatternKit.Examples.AbstractFactoryDemo.AbstractFactoryDemo;
using InterpreterRulesDemo = PatternKit.Examples.InterpreterDemo.InterpreterDemo;

namespace PatternKit.Examples.DependencyInjection;

Expand Down Expand Up @@ -98,6 +100,7 @@ public sealed record GeneratedMailboxExample(MailboxExampleRunner Runner);
public sealed record GeneratedReliabilityPipelineExample(ReliabilityExampleRunner Runner);
public sealed record ResilientCheckoutMailboxesExample(Func<CheckoutRequest, CheckoutServices, CheckoutResult> Run);
public sealed record MessagingBackplaneFacadeExample(Func<CancellationToken, ValueTask<BackplaneDemoSummary>> RunAsync);
public sealed record GeneratedInterpreterRulesExample(Interpreter<InterpreterRulesDemo.PricingContext, decimal> Pricing, Interpreter<InterpreterRulesDemo.PricingContext, bool> Eligibility);
public sealed record PrototypeGameCharacterFactoryExample(Prototype<string, PrototypeDemo.PrototypeDemo.GameCharacter> Factory);
public sealed record ProxyPatternDemonstrationsExample(Proxy<int, string> RemoteProxy, Proxy<(string To, string Subject, string Body), bool> EmailProxy);
public sealed record FlyweightGlyphCacheExample(Func<string, IReadOnlyList<(FlyweightDemo.FlyweightDemo.Glyph Glyph, int X)>> RenderSentence);
Expand Down Expand Up @@ -143,6 +146,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddGeneratedReliabilityPipelineExample()
.AddResilientCheckoutMailboxesExample()
.AddMessagingBackplaneFacadeExample()
.AddGeneratedInterpreterRulesExample()
.AddPrototypeGameCharacterFactoryExample()
.AddProxyPatternDemonstrationsExample()
.AddFlyweightGlyphCacheExample()
Expand Down Expand Up @@ -423,6 +427,16 @@ public static IServiceCollection AddMessagingBackplaneFacadeExample(this IServic
return services.RegisterExample<MessagingBackplaneFacadeExample>("Messaging Backplane Facade", ExampleIntegrationSurface.GenericHost | ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.ExternalInfrastructure | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddGeneratedInterpreterRulesExample(this IServiceCollection services)
{
services.AddSingleton(static _ => InterpreterRulesDemo.CreateGeneratedPricingInterpreter());
services.AddSingleton(static _ => InterpreterRulesDemo.CreateGeneratedEligibilityInterpreter());
services.AddSingleton<GeneratedInterpreterRulesExample>(sp => new(
sp.GetRequiredService<Interpreter<InterpreterRulesDemo.PricingContext, decimal>>(),
sp.GetRequiredService<Interpreter<InterpreterRulesDemo.PricingContext, bool>>()));
return services.RegisterExample<GeneratedInterpreterRulesExample>("Generated Interpreter Rules", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddPrototypeGameCharacterFactoryExample(this IServiceCollection services)
{
services.AddSingleton(_ => PrototypeDemo.PrototypeDemo.CreateCharacterFactory());
Expand Down
113 changes: 113 additions & 0 deletions src/PatternKit.Examples/InterpreterDemo/InterpreterDemo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using PatternKit.Behavioral.Interpreter;
using PatternKit.Generators.Interpreter;
using static PatternKit.Behavioral.Interpreter.ExpressionExtensions;

namespace PatternKit.Examples.InterpreterDemo;
Expand Down Expand Up @@ -120,6 +121,12 @@ public static Interpreter<PricingContext, decimal> CreatePricingInterpreter()
.Build();
}

/// <summary>
/// Creates the source-generated pricing interpreter used by importable examples and IoC integrations.
/// </summary>
public static Interpreter<PricingContext, decimal> CreateGeneratedPricingInterpreter()
=> GeneratedPricingRuleInterpreter.Create();

/// <summary>
/// Creates an async interpreter for rules that need external lookups.
/// </summary>
Expand Down Expand Up @@ -194,6 +201,12 @@ public static Interpreter<PricingContext, bool> CreateEligibilityInterpreter()
.Build();
}

/// <summary>
/// Creates the source-generated eligibility interpreter used by importable examples and IoC integrations.
/// </summary>
public static Interpreter<PricingContext, bool> CreateGeneratedEligibilityInterpreter()
=> GeneratedEligibilityRuleInterpreter.Create();

// ─────────────────────────────────────────────────────────────────────────
// Sample Rule Expressions
// ─────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -374,3 +387,103 @@ public static async Task RunAsync()

public static void Run() => RunAsync().GetAwaiter().GetResult();
}

[GenerateInterpreter(typeof(InterpreterDemo.PricingContext), typeof(decimal), FactoryMethodName = "Create")]
public static partial class GeneratedPricingRuleInterpreter
{
[InterpreterTerminal("number")]
private static decimal Number(string token) => decimal.Parse(token);

[InterpreterTerminal("percent")]
private static decimal Percent(string token) => decimal.Parse(token.TrimEnd('%')) / 100m;

[InterpreterTerminal("var")]
private static decimal Variable(string token, InterpreterDemo.PricingContext context)
=> token switch
{
"cart_total" => context.CartTotal,
"item_count" => context.ItemCount,
"tier_discount" => context.CustomerTier switch
{
"Gold" => 0.10m,
"Platinum" => 0.15m,
"Diamond" => 0.20m,
_ => 0.0m
},
_ => context.Variables.TryGetValue(token, out var value) ? value : 0m
};

[InterpreterNonTerminal("add")]
private static decimal Add(decimal[] args) => args[0] + args[1];

[InterpreterNonTerminal("div")]
private static decimal Divide(decimal[] args) => args[1] != 0 ? args[0] / args[1] : 0m;

[InterpreterNonTerminal("eq")]
private static decimal Equal(decimal[] args) => args[0] == args[1] ? 1m : 0m;

[InterpreterNonTerminal("gt")]
private static decimal GreaterThan(decimal[] args) => args[0] > args[1] ? 1m : 0m;

[InterpreterNonTerminal("if")]
private static decimal If(decimal[] args)
{
if (args.Length != 3)
throw new InvalidOperationException("if requires 3 arguments: condition, then, else");
return args[0] > 0 ? args[1] : args[2];
}

[InterpreterNonTerminal("lt")]
private static decimal LessThan(decimal[] args) => args[0] < args[1] ? 1m : 0m;

[InterpreterNonTerminal("max")]
private static decimal Max(decimal[] args) => Math.Max(args[0], args[1]);

[InterpreterNonTerminal("min")]
private static decimal Min(decimal[] args) => Math.Min(args[0], args[1]);

[InterpreterNonTerminal("mul")]
private static decimal Multiply(decimal[] args) => args[0] * args[1];

[InterpreterNonTerminal("round")]
private static decimal Round(decimal[] args) => Math.Round(args[0], 2, MidpointRounding.AwayFromZero);

[InterpreterNonTerminal("sub")]
private static decimal Subtract(decimal[] args) => args[0] - args[1];
}

[GenerateInterpreter(typeof(InterpreterDemo.PricingContext), typeof(bool), FactoryMethodName = "Create")]
public static partial class GeneratedEligibilityRuleInterpreter
{
[InterpreterTerminal("bool")]
private static bool Boolean(string token) => bool.Parse(token);

[InterpreterTerminal("cartOver")]
private static bool CartOver(string token, InterpreterDemo.PricingContext context)
=> context.CartTotal > decimal.Parse(token);

[InterpreterTerminal("isHoliday")]
private static bool IsHoliday(string token, InterpreterDemo.PricingContext context)
=> context.IsHoliday;

[InterpreterTerminal("itemsOver")]
private static bool ItemsOver(string token, InterpreterDemo.PricingContext context)
=> context.ItemCount > int.Parse(token);

[InterpreterTerminal("promo")]
private static bool Promo(string token, InterpreterDemo.PricingContext context)
=> context.PromoCode.Equals(token, StringComparison.OrdinalIgnoreCase);

[InterpreterTerminal("tier")]
private static bool Tier(string token, InterpreterDemo.PricingContext context)
=> context.CustomerTier.Equals(token, StringComparison.OrdinalIgnoreCase);

[InterpreterNonTerminal("and")]
private static bool And(bool[] args) => args[0] && args[1];

[InterpreterNonTerminal("not")]
private static bool Not(bool[] args) => !args[0];

[InterpreterNonTerminal("or")]
private static bool Or(bool[] args) => args[0] || args[1];
}
Loading
Loading