D Code Style Guide
Follow the official DStyle (local copy: dstyle.md). Key points:
- Whitespace: 4-space indentation, one statement per line
- Braces: Own line for functions and type definitions
- Naming:
camelCasefor constants/enum members/variables/functions/UDAs,PascalCasefor types,snake_casefor modules, if a name would conflict with a keyword append_, e.g.,class_, all letters in acronyms should have the same case, e.g.,xmlLoad,parseXML
Module Layout
Organize D modules in this order:
- Module-level Ddoc - Documentation for the entire module
- Module declaration -
module sparkles.core_cli.example; - Imports - Grouped as described below
- Ddoc-ed module-level unit tests - Usage examples for the module as a whole
- Public API - Most important user-facing items first (public aliases, types, functions)
- Implementation details - Private aliases, types, functions
- Non-Ddoc module-level unit tests - Integration tests using multiple module members
Unit tests for a specific function or type should follow that declaration directly.
See DDoc for documentation comment syntax and conventions.
Imports
Group imports in this order, separated by a single empty line between groups:
core.*modules (DRuntime)std.*modules (Phobos)- External dependencies
- Modules from other sub-packages of this project
- Modules from the same sub-package
import core.memory : pureMalloc, pureFree;
import std.range.primitives : put, empty, front, popFront;
import std.traits : isSomeChar, isSomeString, isNumeric;
import sparkles.core_cli.term_style : Style, stylize;Import Best Practices
- Always use selective imports - Import only the symbols you need, not entire modules
- Prefer local (scoped) imports - Use function-level or type-level imports for clarity, similar to how variables should have the smallest possible scope. Bonus: templates with scoped imports that are never instantiated won't trigger the import
- Use renamed imports to avoid name clashes or improve clarity:
import std.file : writeFile = write; // Avoid clash with std.stdio.writeEponymous Templates
Use short eponymous template syntax:
// Good
enum isSpecial(T) = is(T == int) || is(T == long);
// Avoid
template isSpecial(T)
{
enum isSpecial = is(T == int) || is(T == long);
}Expression-Based Contracts (DIP1009)
// Good
int divide(int a, int b)
in (b != 0)
out (r; r * b == a)
{
return a / b;
}
// Avoid
int divide(int a, int b)
in
{
assert(b != 0);
}
out (r)
{
assert(r * b == a);
}
do
{
return a / b;
}Expression-Based Functions (DIP1043)
For simple functions, use => syntax:
// Good
int square(int x) => x * x;
// Avoid
int square(int x)
{
return x * x;
}Static Foreach
Use static foreach over tuples and AliasSeq:
// Good
static foreach (T; AliasSeq!(int, long, float))
{
pragma(msg, T.stringof);
}
// Avoid
foreach (T; AliasSeq!(int, long, float))
{
pragma(msg, T.stringof);
}Copy Constructors (DIP1018)
Use copy constructors instead of postblit:
struct S
{
int* ptr;
// Good
this(ref return scope const S another)
{
ptr = new int(*another.ptr);
}
// Avoid
// this(this) { ptr = new int(*ptr); }
}Input Parameters
Use in for read-only parameters (implies const scope):
// Good
void process(in Config config) { ... }
// Avoid
void process(const ref Config config) { ... }Note: in may be omitted for primitive types and immutable(T)[] slices (e.g., string).
Named Arguments (DIP1030)
Use named arguments for clarity at call sites:
auto result = createWidget(
width: 100,
height: 200,
visible: true,
resizable: false,
);Forcing Named Arguments
Force external callers to use named arguments by adding a private-typed sentinel with a default value as the first parameter. Callers outside the module cannot construct the private type, so positional calls fail at compile time while named calls skip past the sentinel via its default:
private struct NamedOnly {}
void draw(NamedOnly _ = NamedOnly.init, int x = 0, int y = 0, int width = 0, int height = 0)
{
// ...
}
// From another module:
draw(x: 10, y: 20, width: 100, height: 200); // ✅
draw(10, 20, 100, 200); // ❌ compile errorThe same technique applies to struct fields. For the function-parameter variant the sentinel is zero-cost — it produces identical assembly to a plain function. See Forcing Named Arguments for the full write-up including ABI analysis, struct caveats, and alternative techniques that were evaluated.
Interpolated Expression Sequences (DIP1036)
Use IES (i"...") when interspersing string literals with expressions. Preference order:
- IES — Type-safe, enables context-aware encoding
std.format— When format specifiers are needed (%08x,%.2f)- Manual concatenation — Avoid
import std.conv : text;
import std.stdio : writeln;
string name = "Alice";
int count = 42;
// Good: IES with writeln (no allocation)
writeln(i"Hello, $(name)! Count: $(count)");
// Good: IES converted to string
string msg = i"Hello, $(name)! Count: $(count)".text;
// Good: std.format when format specifiers needed
import std.format : format;
string hex = format!"Value: 0x%08X"(count);
// Avoid: manual concatenation
string bad = "Hello, " ~ name ~ "! Count: " ~ count.to!string;Key rules:
- IES produces a tuple, not a string — use
.textor pass to IES-accepting functions - Prefer
writeln(i"...")overwriteln(i"...".text)to avoid allocation - For security-sensitive contexts (SQL, HTML, URLs), use dedicated IES-processing functions that escape interpolated values
See Interpolated Expression Sequences for complete patterns including safe SQL queries, HTML templates, and structured logging.