Skip to content

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: camelCase for constants/enum members/variables/functions/UDAs, PascalCase for types, snake_case for 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:

  1. Module-level Ddoc - Documentation for the entire module
  2. Module declaration - module sparkles.core_cli.example;
  3. Imports - Grouped as described below
  4. Ddoc-ed module-level unit tests - Usage examples for the module as a whole
  5. Public API - Most important user-facing items first (public aliases, types, functions)
  6. Implementation details - Private aliases, types, functions
  7. 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:

  1. core.* modules (DRuntime)
  2. std.* modules (Phobos)
  3. External dependencies
  4. Modules from other sub-packages of this project
  5. Modules from the same sub-package
d
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:
d
import std.file : writeFile = write;  // Avoid clash with std.stdio.write

Eponymous Templates

Use short eponymous template syntax:

d
// 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)

d
// 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:

d
// Good
int square(int x) => x * x;

// Avoid
int square(int x)
{
    return x * x;
}

Static Foreach

Use static foreach over tuples and AliasSeq:

d
// 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:

d
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):

d
// 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:

d
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:

d
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 error

The 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:

  1. IES — Type-safe, enables context-aware encoding
  2. std.format — When format specifiers are needed (%08x, %.2f)
  3. Manual concatenation — Avoid
d
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 .text or pass to IES-accepting functions
  • Prefer writeln(i"...") over writeln(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.