Waffle.Core 1.0.1

dotnet add package Waffle.Core --version 1.0.1
                    
NuGet\Install-Package Waffle.Core -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Waffle.Core" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Waffle.Core" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Waffle.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Waffle.Core --version 1.0.1
                    
#r "nuget: Waffle.Core, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Waffle.Core@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Waffle.Core&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Waffle.Core&version=1.0.1
                    
Install as a Cake Tool

Waffle.Core

The core engine of the Waffle — a pure C# template engine that embeds flow control directly into interpolated string literals via a customized InterpolatedStringHandler.

Syntax Reference

See docs/syntax-reference.md for the full syntax reference.

Architecture

High-Level Overview

Waffle works by hijacking C#'s interpolated string machinery. When you write:

Render(ctx, $"...{For(0, n, out var i)}...{End}...");

The compiler expands this into a series of AppendLiteral / AppendFormatted calls on a TemplateInterpreter struct (marked with [InterpolatedStringHandler]). Instead of immediately producing output, these calls build a flat token list that is later evaluated as a syntax tree.

Processing Phases

Source code (interpolated string)
        │
        ▼
┌─────────────────────────┐
│  1. PARSING             │  AppendLiteral / AppendFormatted calls
│     TemplateInterpreter │  → builds flat List<Token>
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  2. BLOCK PAIRING       │  PairBlockIndexes()
│     TemplateEvaluator   │  → links For↔End, If↔Elif↔Else↔End
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  3. TRIM / WHITESPACE   │  ApplyTrims()
│     TemplateEvaluator   │  → removes command-only lines, applies < > directives
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  4. EVALUATION          │  EvaluateRange()
│     TemplateEvaluator   │  → walks tokens, executes blocks, resolves values
│                         │  → appends output via ITemplateInterpreterContext.Append()
└─────────────────────────┘

Key Types

Type Kind Role
WaffleSyntax static class Public API surface — For, ForEach, If, Elif, Else, End, Render, etc.
TemplateInterpreter struct [InterpolatedStringHandler] that parses interpolated strings into tokens.
InstantTemplateInterpreter struct Variant that returns the result string directly (no external context needed).
TemplateInterpreterController class Mutable state container: token list, block stack, consumption counters.
TemplateEvaluator struct Pairs blocks, applies trims, and evaluates the token list into output.
Token class A single element (literal or interpolation) in the flat token list. Pooled for reuse.
ITemplateInterpreterContext interface Receives output and lifecycle hooks during evaluation.
IBlockContent interface Any interpolated value that resolves to output (tokens, pipelines).
IResolvableTo<T> interface A lazily-resolved value — evaluated only at render time using the environment dictionary.
IIterationSource<TIterator, T> interface A list with typed iterators for use in ForEach loops.
EnvValue struct Tagged union (int or object?) for the environment dictionary, avoiding boxing for loop counters.

Directory Layout

Waffle.Core/
├── WaffleSyntax.cs                          # Public static API
├── Interpreter/
│   ├── Interfaces/
│   │   ├── ITemplateInterpreterContext.cs   # Output + lifecycle hooks
│   │   ├── IResolvableTo.cs                 # Lazy resolution abstraction + pipeline extensions
│   │   ├── IIterationSource.cs              # Typed iteration source + LINQ-like extensions
│   │   └── ILazyInitializedBy.cs            # Workaround for abstract static on netstandard2.0
│   ├── Impls/
│   │   ├── TemplateInterpreter.cs           # Parser (InterpolatedStringHandler)
│   │   ├── InstantTemplateInterpreter.cs    # Self-contained variant (no external context)
│   │   ├── InstantTemplateContext.cs        # Simple context for InstantTemplateInterpreter
│   │   ├── TemplateInterpreterController.cs # Mutable parsing state
│   │   ├── TemplateEvaluator.cs             # Block pairing + trim + evaluation
│   │   ├── TemplateInterpreterHelper.cs     # Formatting utilities
│   │   ├── Token.cs                         # Token data + object pool
│   │   ├── FlowControl.cs                   # Break/Continue signal enum
│   │   └── TrimMode.cs                      # Whitespace trim directives
│   ├── Block/
│   │   ├── IBlockContent.cs                 # Evaluable block element interface
│   │   ├── BlockCategory.cs                 # Conditional vs Iteration
│   │   └── EvalResult.cs                    # Stringified evaluation result
│   ├── Interpolation/
│   │   ├── Command/
│   │   │   ├── Iteration/                   # For, ForEach, Break, Continue commands
│   │   │   └── Condition/                   # If, Elif, Else commands
│   │   ├── Pipeline/                        # SelectPipe, CombinePipe, ExtractPipe, etc.
│   │   └── Proxy/                           # IntProxy, StringProxy, BoolProxy, ListProxy, etc.
│   ├── EnvValue.cs                          # Tagged-union for environment values
│   └── TemplateError.cs                     # Error reporting record
├── Helper/                                  # Object pools (List, Stack, StringBuilder, etc.)
└── Polyfills/                               # netstandard2.0 backports (Index, Range, attributes)

Lazy Resolution & Environment

Loop variables in Waffle are not captured by closures. Instead, they are stored in an environment dictionary (Dictionary<int, EnvValue>) keyed by a generated integer ID. At evaluation time, IResolvableTo<T>.Resolve(env) reads the current value from this dictionary.

This design enables composable pipelines via .To(), .With(), .Of(), .Extract(), .Replace() and is supported by type-safe deferred wrappers generated by Waffle.ModelProxy.

Extensibility Points

Extension Point How to Use
ITemplateInterpreterContext Implement to control output destination, intercept appends, handle errors, or add pre/post hooks.
IBlockContent Implement to create custom interpolation objects that evaluate to strings at render time.
TryHandleUnhandledInterpolation Override in your context to support custom types in interpolation holes.
OnPreAppendLiteral / OnPreAppendFormatted Override to transform content before it is appended (e.g., escaping).

Threading Model

WaffleSyntax uses [ThreadStatic] fields (s_lastUsedId) for ID generation. The TemplateInterpreter struct is designed for single-threaded use within one template rendering call. Multiple templates may be rendered concurrently on separate threads, each with its own ITemplateInterpreterContext instance.

Performance Considerations

  • TemplateInterpreter is a struct to avoid heap allocation on every render call.
  • Token objects are pooled via a thread-static stack to minimize GC pressure.
  • EnvValue stores int values inline (no boxing) for loop counters.
  • Collection helpers (ListPool, StackPool, StringBuilderPool, DictionaryPool) rent and return buffers to avoid repeated allocation.
  • The library targets netstandard2.0 with polyfills, keeping it compatible with Incremental Source Generators, Unity, and older runtimes while using C# 14 language features via the compiler.

Using Waffle.Core inside a Source Generator project

When you reference Waffle.Core from your own Incremental Source Generator project, consumers of that generator may fail to build because the Roslyn compilation context does not automatically resolve the transitive Waffle.Core.dll dependency at generator-execution time.

To fix this, add the following to your generator's .csproj so that Waffle.Core.dll is explicitly declared as a generator dependency:

<ItemGroup>
    <PackageReference Include="Waffle.Core" Version="1.x" GeneratePathProperty="true"/>
</ItemGroup>
<PropertyGroup>
    <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
    <TargetPathWithTargetPlatformMoniker
        Include="$(PkgWaffle_Core)\lib\netstandard2.0\Waffle.Core.dll"
        IncludeRuntimeDependency="false"/>
</Target>

This applies to any project that uses Waffle.Core as a generator-side dependency, regardless of whether Waffle.ModelProxy is also in use.

Dependencies

No external dependencies.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Waffle.Core:

Package Downloads
Waffle.Bakery

Provides a foundational implementation of a typical design-time code generation application that executes multiple Waffle templates.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 256 6/10/2026
1.0.0 132 5/27/2026