D Developer Guidelines: Interpolated Expression Sequences (IES)
Overview
Interpolated expression sequences (IES) embed D expressions directly in string literals using $(expr) syntax. Unlike simple string interpolation in other languages, D's IES preserves metadata about each component—literal text segments and expression source code—enabling context-aware processing at compile time.
Key benefits:
- Type safety — Functions receive actual typed values, not stringified text
- Metadata preservation — Libraries can distinguish literals from dynamic values
- Context-aware encoding — Apply URL encoding, HTML escaping, or SQL parameterization based on context
- Compile-time validation — Validate structure (HTML tags, SQL syntax) before runtime
- Zero overhead — Compile-time string generation has no runtime cost
Relationship to Other Guidelines
- Code Style — Formatting, naming, and syntax conventions
- Functional & Declarative Programming — Range pipelines, output ranges, purity
- DDoc — Documentation comments;
$(...)syntax overlap with IES
Quick Reference
// IES is written with the i"..." prefix. It does NOT produce a string.
// It produces a compile-time sequence of typed segments.
import core.interpolation; // auto-imported when IES is used, but explicit is clearer
string name = "Alice";
int count = 42;
// Convert to string explicitly
import std.conv : text;
string greeting = i"Hello, $(name)! You have $(count) items.".text;
// Pass directly to functions that accept IES (no allocation)
import std.stdio : writeln;
writeln(i"Hello, $(name)! You have $(count) items.");
// WRONG — IES does not implicitly convert to string
// string s = i"Hello, $(name)"; // COMPILE ERROR1. IES Literal Syntax
IES has three literal forms. All use $(expr) for interpolation.
Syntax Forms
| Form | Delimiter | Escapes? | $(...) interpolation? |
|---|---|---|---|
i"..." | Double quotes | Yes (\n, \t, \$, etc.) | Yes |
i`...` | Backticks | No | Yes |
iq{...} | iq{ } | No | Yes |
Note: IES literals do NOT support character width suffixes (no i"..."w or i"..."d).
Double-Quoted IES
Standard form. Supports escape sequences (\n, \t, \\, \$, etc.).
string msg = i"Name:\t$(name)\nCount:\t$(count)".text;
// \$ produces a literal $ character
string price = i"Price: \$$(amount)".text;A bare $ NOT followed by ( is treated as a literal $ — no escape needed:
string s = i"costs $5 but $(variable) is interpolated".text;Wysiwyg IES
Backtick-delimited. No escape sequences are recognized. What you type is what you get.
string raw = i`no escapes here: \n is literal, $(expr) is interpolated`.text;Token IES
Must contain valid D tokens. No escape sequences. Useful for embedding D code fragments.
auto tokens = iq{$(expr) + $(other)};2. Converting IES to a String
IES does not implicitly convert to string. This is a deliberate safety decision to prevent injection vulnerabilities.
Using std.conv.text (Simple Cases)
import std.conv : text;
string name = "Alice";
string result = i"Hello, $(name)!".text;
assert(result == "Hello, Alice!");Using std.stdio.writeln (Direct Output, No Allocation)
import std.stdio : writeln;
writeln(i"Value: $(x)"); // No string allocation — writes segments directly@nogc Considerations
.text allocates via GC. For @nogc code, write to output ranges instead (see Output Range Integration).
Rule: Prefer passing IES directly to functions that accept it natively (like writeln) over converting to string first. This avoids unnecessary allocation and enables type-safe processing.
3. What IES Produces
An IES literal is not a string. The compiler transforms it into a sequence of typed segments. Every IES sequence has this structure:
InterpolationHeader, ...segments..., InterpolationFooterEach segment is one of:
| Segment Type | What It Represents | toString() Returns |
|---|---|---|
InterpolationHeader | Start sentinel | "" (empty) |
InterpolatedLiteral!"text" | Literal string portion | The literal text |
InterpolatedExpression!"code" | Source text of next expression | "" (empty) |
| (the actual value) | The runtime value of the expression | (its own type) |
InterpolationFooter | End sentinel | "" (empty) |
Concrete Expansion Example
string name = "Alice";
int count = 3;
auto ies = i"$(name) has $(count) items.";
// The compiler expands this to a sequence equivalent to:
// (
// InterpolationHeader(),
// InterpolatedExpression!"name"(), // source text of expression
// name, // actual value: "Alice"
// InterpolatedLiteral!" has "(), // literal text between expressions
// InterpolatedExpression!"count"(), // source text of expression
// count, // actual value: 3
// InterpolatedLiteral!" items."(), // trailing literal text
// InterpolationFooter()
// )Key Properties
InterpolatedLiteralcarries its string as a compile-time template parameter, accessible via.toString()orisexpression.InterpolatedExpressioncarries the source code of the expression as a compile-time template parameter, accessible via.expressionenum.- The actual runtime values appear directly in the sequence after their corresponding
InterpolatedExpression. core.interpolationtypes are automatically imported when IES is used, but explicitimport core.interpolation;is recommended for clarity in library code.- None of these structs carry runtime state.
Security: Why IES Does Not Convert to String Implicitly
This segment structure is the foundation of IES's security model. Because each segment carries its type, library functions can distinguish trusted literals from untrusted values.
The type system distinguishes between literal text (trusted, from source code) and interpolated values (untrusted, from variables). This is a critical safety property.
The Problem with Naive String Building
// Dangerous: manual string concatenation has no type-level safety
string query = "SELECT * FROM users WHERE name = '" ~ userInput ~ "'";
// If userInput = "'; DROP TABLE users; --" -> SQL injectionThe IES Advantage
A library function receiving an IES can inspect each segment's type at compile time:
// The library sees:
// InterpolatedLiteral!"SELECT * FROM users WHERE name = '" -> trusted literal
// InterpolatedExpression!"userInput" -> marker
// userInput -> UNTRUSTED value: escape it!
// InterpolatedLiteral!"'" -> trusted literalThis enables the library to automatically apply context-appropriate escaping (SQL parameterization, HTML encoding, URL encoding, shell escaping) without any effort from the caller.
Use Cases Enabled by This Design
| Domain | What the library does with interpolated values |
|---|---|
| SQL | Parameterized queries (bind variables) |
| HTML | HTML-entity encoding |
| URLs | Percent-encoding |
| Shell commands | Shell escaping |
| Logging | Structured field extraction |
| i18n/l10n | Reorderable message parameters |
Rule: Library authors SHOULD provide IES-accepting overloads rather than requiring callers to use .text. This preserves safety and avoids unnecessary allocation.
Rule: Library authors MUST NOT mixin() the string from InterpolatedExpression. It comes from a different scope and will fail. The string is informational only.
4. Writing Functions That Accept IES
Recommended Pattern: Variadic Template with Header/Footer Guards
import core.interpolation;
void processIES(Sequence...)(
InterpolationHeader,
Sequence data,
InterpolationFooter
)
{
// Process `data` here.
// `data` contains interleaved InterpolatedLiteral, InterpolatedExpression,
// and actual values.
}
// Usage:
string name = "Alice";
processIES(i"Hello, $(name)!");The InterpolationHeader/InterpolationFooter parameters act as type-level guards that ensure the function is only called with an IES, and they delimit the interpolation boundary.
Iterating Over Segments at Compile Time
import core.interpolation;
import std.array : appender;
string iesConcat(Sequence...)(InterpolationHeader, Sequence data, InterpolationFooter)
{
import std.conv : to;
auto result = appender!string;
static foreach (item; data)
{
// InterpolatedLiteral — a literal string fragment
static if (is(typeof(item) == InterpolatedLiteral!str, string str))
{
result ~= str;
}
// InterpolatedExpression — skip (it's metadata only)
else static if (is(typeof(item) == InterpolatedExpression!code, string code))
{
// `code` contains the source text, e.g. "name"
// Typically skip this; it's informational only.
}
// Actual runtime value
else
{
result ~= to!string(item);
}
}
return result[];
}Compile-Time Template Parameter Usage
IES can also be passed as template arguments, enabling compile-time processing including types:
import core.interpolation;
template processAtCompileTime(InterpolationHeader header, Sequence...)
{
static assert(Sequence[$ - 1] == InterpolationFooter());
// Process Sequence at compile time...
}
alias result = processAtCompileTime!(i"Type is: $(int)");Output Range Integration
The patterns above use appender!string as the output sink. For @nogc contexts or streaming output, the same approach works with output ranges.
Writing to Output Ranges
import core.interpolation;
import std.conv : to;
import std.range.primitives : isOutputRange, put;
void writeInterpolated(Writer, Args...)(
ref Writer w,
InterpolationHeader,
Args args,
InterpolationFooter
)
if (isOutputRange!(Writer, char))
{
static foreach (arg; args)
{
static if (is(typeof(arg) == InterpolatedLiteral!str, string str))
put(w, str);
else static if (is(typeof(arg) == InterpolatedExpression!expr, string expr))
{
// Skip metadata
}
else
put(w, arg.to!string);
}
}For a production implementation of this pattern, see the styled_template module, where writeStyled() processes IES into styled terminal output via an output range.
@nogc with SmallBuffer
For @nogc contexts, use SmallBuffer and avoid .to!string:
import core.interpolation;
import sparkles.core_cli.smallbuffer : SmallBuffer;
// Usage
SmallBuffer!(char, 256) buf;
// Custom @nogc IES processing with integer-to-string conversion
// that doesn't allocate...5. Complete Patterns
The patterns in this section are adapted from Adam D. Ruppe's interpolation-examples repository, which demonstrates real-world IES use cases.
Pattern: Safe SQL Query Builder
import core.interpolation;
struct SafeQuery
{
string sql;
string[] params;
}
SafeQuery buildQuery(Sequence...)(InterpolationHeader, Sequence data, InterpolationFooter)
{
import std.conv : to;
import std.array : appender;
auto sql = appender!string;
string[] params;
static foreach (item; data)
{
static if (is(typeof(item) == InterpolatedLiteral!str, string str))
{
sql ~= str;
}
else static if (is(typeof(item) == InterpolatedExpression!code, string code))
{
// skip expression metadata
}
else
{
sql ~= "?";
params ~= to!string(item);
}
}
return SafeQuery(sql[], params);
}
// Usage:
string userName = "Alice'; DROP TABLE users; --";
auto q = buildQuery(i"SELECT * FROM users WHERE name = $(userName)");
assert(q.sql == "SELECT * FROM users WHERE name = ?");
assert(q.params == ["Alice'; DROP TABLE users; --"]);Attribution
Adapted from lib/sql.d in Adam D. Ruppe's interpolation-examples. The original uses compile-time query construction with execi() for a real SQLite binding.
Pattern: HTML Template with Auto-Escaping
import core.interpolation;
import std.array : appender, replace;
string htmlEscape(string s)
{
return s.replace("&", "&").replace("<", "<").replace(">", ">")
.replace("\"", """).replace("'", "'");
}
string safeHtml(Sequence...)(InterpolationHeader, Sequence data, InterpolationFooter)
{
auto result = appender!string;
static foreach (item; data)
{
static if (is(typeof(item) == InterpolatedLiteral!str, string str))
{
result ~= str; // Trusted literal: pass through
}
else static if (is(typeof(item) == InterpolatedExpression!code, string code))
{
// skip
}
else static if (is(typeof(item) : string))
{
result ~= htmlEscape(item); // Untrusted value: escape!
}
else
{
import std.conv : to;
result ~= htmlEscape(to!string(item));
}
}
return result[];
}
// Usage:
string userInput = "<script>alert('xss')</script>";
string html = safeHtml(i"<p>Hello, $(userInput)!</p>");
// Result: <p>Hello, <script>alert('xss')</script>!</p>Attribution
Adapted from lib/html.d in Adam D. Ruppe's interpolation-examples. The original includes compile-time HTML structure validation and returns a DOM Element.
Pattern: URL Encoding
import core.interpolation;
import std.array : appender;
import std.conv : to;
import std.uri : encodeComponent;
string urlSafe(Args...)(InterpolationHeader, Args args, InterpolationFooter)
{
auto result = appender!string;
static foreach (arg; args)
{
static if (is(typeof(arg) == InterpolatedLiteral!str, string str))
result ~= str;
else static if (is(typeof(arg) == InterpolatedExpression!expr, string expr))
{
// Skip metadata
}
else
result ~= encodeComponent(arg.to!string);
}
return result[];
}
// Usage
auto query = "hello world & goodbye";
auto url = urlSafe(i"https://example.com/search?q=$(query)");
// Result: "https://example.com/search?q=hello%20world%20%26%20goodbye"Attribution
Adapted from lib/url.d in Adam D. Ruppe's interpolation-examples. The original uses a state machine to apply context-appropriate encoding to different URL components.
Pattern: Structured Logging
import core.interpolation;
import std.conv : to;
import std.array : appender;
void logStructured(Sequence...)(InterpolationHeader, Sequence data, InterpolationFooter)
{
auto message = appender!string;
string[string] fields;
static foreach (idx, item; data)
{
static if (is(typeof(item) == InterpolatedLiteral!str, string str))
{
message ~= str;
}
else static if (is(typeof(item) == InterpolatedExpression!code, string code))
{
// The next element in the sequence is the actual value.
// Store as a named field for structured logging.
fields[code] = to!string(data[idx + 1]);
}
else
{
message ~= to!string(item);
}
}
import std.stdio : writefln;
writefln!"msg=%s fields=%s"(message[], fields);
}
// Usage:
string user = "Alice";
int latency = 42;
logStructured(i"Request from $(user) took $(latency)ms");
// Output: msg=Request from Alice took 42ms fields=["user": "Alice", "latency": "42"]Attribution
Inspired by the internationalization example (04-internationalization.d) in Adam D. Ruppe's interpolation-examples, which uses InterpolatedExpression to extract named parameters for reorderable message templates.
Real-World: styled_template
The sparkles styled_template module applies the same IES patterns shown above to build a styled terminal output system. Its core function, writeStyled(), accepts IES and writes ANSI-styled text to any output range:
void writeStyled(Writer, Args...)(
ref Writer w,
InterpolationHeader,
Args args,
InterpolationFooter
)
{
import std.conv : to;
import std.range.primitives : put;
ParserContext ctx;
static foreach (arg; args)
{{
alias T = typeof(arg);
static if (is(T == InterpolatedLiteral!lit, string lit))
{
parseLiteral(w, lit, ctx);
}
else static if (is(T == InterpolatedExpression!code, string code))
{
// Skip expression metadata
}
else
{
// Output interpolated value - styles already active from block
put(w, arg.to!string);
}
}}
}The three-branch static if dispatch — literal, expression metadata, runtime value — is the same core pattern used in every example above. The difference is that parseLiteral interprets a {style ...} mini-language within literal segments to emit ANSI escape codes.
Consumer-side examples:
libs/core-cli/examples/styled_template.d— comprehensive demo of style syntax (colors, bold, nesting, negation)libs/core-cli/examples/box.d— box drawing with styled IES contentlibs/core-cli/examples/table.d— table rendering with styled headersscripts/run_md_examples.d— CLI status and progress output using IES
6. When to Use IES vs Alternatives
Use IES When
- Building user-facing messages with embedded values
- Context-aware encoding is needed (SQL, HTML, URLs)
- You want compile-time metadata about the template structure
- Passing to functions that process the sequence directly
Use std.format / writef When
- You need format specifiers (
%08x,%.2f,%10s) - Building strings for
printf-style APIs - Maximum control over output formatting
Decision Matrix
| Scenario | Recommendation |
|---|---|
| CLI output messages | IES with writeln |
| SQL queries | IES with parameterization |
| HTML generation | IES with entity encoding |
| Hex/binary formatting | std.format |
| Log messages with values | IES |
| @nogc string building | IES with output ranges |
7. Common Mistakes
Assigning IES Directly to a String
string s = i"Hello, $(name)"; // COMPILE ERRORFix: Use .text or pass to an IES-accepting function:
import std.conv : text;
string s = i"Hello, $(name)".text;Trying to Mixin an InterpolatedExpression
// Inside a library function processing IES:
mixin(code); // FAILS — wrong scopeFix: Use the actual runtime value that follows the InterpolatedExpression in the sequence. Never mixin the expression string.
Assuming IES is a Single Value
auto x = i"Hello $(name)";
writeln(typeof(x).stringof); // It's a sequence, not a string!Fix: Understand that x is a sequence. Index it or pass it to a variadic function.
Forgetting that $ Without ( is Literal
string s = i"costs $5".text; // Fine — bare $ is literal
string s = i"costs \$5".text; // Also fine — \$ is explicit escape
string s = i"$(price) USD".text; // Interpolation with $(...)Not Handling Empty InterpolatedExpression
Library code should be robust against implementations that omit InterpolatedExpression or provide an empty string. Do not depend on it always being present:
// Robust: handle both with and without InterpolatedExpression
else static if (is(typeof(item) == InterpolatedExpression!code, string code))
{
// May be empty or missing entirely — handle gracefully
}IES Syntax in DDoc Comments
IES and DDoc both use $(...) syntax. When writing DDoc comments that mention IES, the $(expr) in prose text will be interpreted as a DDoc macro:
/**
Use `i"Hello $(name)"` for interpolation. ← $(name) vanishes!
Use `i"Hello $(DOLLAR)$(LPAREN)name$(RPAREN)"` instead. ← renders correctly
*/This does NOT affect actual IES code in function bodies or unittest bodies — only DDoc comment text. Inside --- code blocks in DDoc comments, $(...) is preserved literally. See DDoc Guidelines for the full interaction rules.
8. Advanced Usage
The following topics cover less common but powerful IES capabilities for library authors and advanced use cases.
Compile-Time Introspection
All IES segment types support compile-time inspection:
string name = "Alice";
auto ies = i"Hello, $(name)!";
// Type checks
static assert(is(typeof(ies[0]) == InterpolationHeader));
static assert(is(typeof(ies[$ - 1]) == InterpolationFooter));
// Literal string access at compile time
static assert(ies[1].toString() == "Hello, ");
// Expression source text access at compile time
static assert(ies[2].expression == "name");
// Runtime value
assert(ies[3] == "Alice");Building Format Strings at Compile Time
import core.interpolation;
template makeFormatString(Args...)
{
enum string makeFormatString = ()
{
string result;
static foreach (arg; Args)
{
static if (is(arg == InterpolatedLiteral!str, string str))
result ~= str;
else static if (is(arg == InterpolatedExpression!expr, string expr))
{
// Skip
}
else static if (is(arg == InterpolationHeader) || is(arg == InterpolationFooter))
{
// Skip
}
else
result ~= "%s"; // Placeholder for value
}
return result;
}();
}
// Validate at compile time with static assert
enum fmt = makeFormatString!(typeof(i"Value: $(42)".expand));
static assert(fmt == "Value: %s");IES and Nesting
IES can nest. Each nested IES produces its own InterpolationHeader/InterpolationFooter pair. Library code processing IES should track nesting depth if it matters:
auto nested = i"outer $(i"inner $(value)") end";
// This produces nested Header/Footer pairs9. Quick-Reference Checklist
- [ ] IES produces a sequence, not a string. Never assign to
stringdirectly. → §3 - [ ] Use
.textfor simple string conversion.import std.conv : text;theni"...".text. → §2 - [ ] Use
writelnfor direct output. It accepts IES natively with zero allocation. → §2 - [ ] Write IES-accepting functions with the
(InterpolationHeader, Sequence data, InterpolationFooter)signature pattern. → §4 - [ ] Distinguish literals from values using
static ifwithis(typeof(item) == InterpolatedLiteral!str, string str). → §4 - [ ] Never mixin InterpolatedExpression. The
.expressionmember is informational only. → §7 - [ ] Escape interpolated values, not literals in security-sensitive contexts (SQL, HTML, URLs). → §3
- [ ] Prefer IES-native overloads over
.textconversion in library APIs. → §3 - [ ]
core.interpolationis auto-imported when IES is used, but import explicitly in library code for clarity. → §3 - [ ] Handle missing/empty
InterpolatedExpression— don't assume it's always present. → §7
References
- D Spec: Interpolation Expression Sequences — Language specification
- core.interpolation module — Runtime support types
- DIP1036 — String Interpolation — Design rationale
- Adam D. Ruppe's interpolation-examples — Real-world IES use cases (SQL, HTML, URLs, i18n)