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