The Hidden toString/formatValue Recursion Trap - And How to Fix It

Aayush Patel itsaayush0711 at gmail.com
Wed Mar 11 14:41:19 UTC 2026


I've discovered that std.format's hasToString detection mechanism 
is fundamentally flawed, forcing developers to use unintuitive 
workarounds. This needs to be fixed at the language/library level.

**The Problem: hasToString Can't Detect Valid toString Methods**

When you implement toString(Writer, Char) that calls formatValue 
internally (a very common pattern), std.format fails to detect it:

---
```
import std.format, std.array, std.variant;

     struct FmtVariant
     {
         private Variant value;

         void toString(Writer, Char)(ref Writer writer, scope 
const ref FormatSpec!Char fmtSpec)
         {
             if (value.type == typeid(int))
                 formatValue(writer, value.get!int, fmtSpec);  // 
Recursively format
         }
     }

     void main()
     {
         auto v = FmtVariant(Variant(42));
         writeln(format("%s", v));  // FAILS: Uses default 
formatting, NOT your toString!
     }
```
---

**Root Cause: Broken Detection Logic**

The hasToString template in std.format uses a dummy lambda to 
test if toString exists:

  ---
  ```
     enum bool hasToString(T, Char) =
         __traits(compiles,
         {
             T val = T.init;
             val.toString((const(char)[] s){}, f);  // Dummy 
lambda writer
         });
  ```
  ---

This creates a catch-22:
1. Dummy lambda only accepts strings, not ints/floats/etc
2. Your toString calls formatValue(dummy, int_value, ...)
3. This FAILS to compile (can't format int into string-only 
lambda)
4. hasToString returns false
5. Your toString is IGNORED

**The Forced Workaround**

You MUST add this hack to make it work:

  ---
  ```
     void toString(Writer, Char)(ref Writer writer, scope const 
ref FormatSpec!Char fmtSpec)
     {
         // REQUIRED HACK: Filter out the dummy lambda
         static if (__traits(compiles, { Writer w; w.put("test"); 
}))
         {
             // Your actual code here
             if (value.type == typeid(int))
                 formatValue(writer, value.get!int, fmtSpec);
         }
     }
  ```
  ---

Why this works:
- Dummy lambda has NO put method → guard fails → body skipped → 
signature still compiles → hasToString succeeds
- Real writers HAVE put method → guard succeeds → code executes

**Why This is a Language/Library Design Flaw**

1. **Non-obvious**: Newcomers have no idea why their toString 
isn't called
2. **Unintuitive**: The workaround makes no semantic sense 
without deep knowledge
3. **Boilerplate**: Every recursive toString needs this guard
4. **Fragile**: Breaks if hasToString implementation changes
5. **Poor diagnostics**: No error message explains what's wrong

**This Should Not Require Workarounds**

The hasToString check should handle recursive formatting 
naturally.


**TL;DR**
```
std.format's hasToString detection is broken for recursive 
formatters. You must add:
static if (__traits(compiles, { Writer w; w.put("test"); }))
to every toString that calls formatValue. This is a design flaw 
that needs fixing.
```
Thoughts? Should I report this as a bug or enhancement request?


More information about the Digitalmars-d-learn mailing list