Skip to content

Commit

Permalink
Merge pull request dlang#5288 from ntrel/ct-format
Browse files Browse the repository at this point in the history
Issue 13568: Add CT-checked format string overloads to std.format
merged-on-behalf-of: Andrei Alexandrescu <andralex@users.noreply.github.com>
  • Loading branch information
dlang-bot committed Mar 19, 2017
2 parents 09ae19f + 1a8d4f6 commit 4a1733e
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 23 deletions.
19 changes: 19 additions & 0 deletions changelog/std-format-formattedWrite.dd
@@ -0,0 +1,19 @@
`std.format.formattedWrite` now accepts a compile-time checked format string

$(REF formattedWrite, std, format) and related functions now have overloads to
take the format string as a compile-time argument. This allows the format
specifiers to be matched against the argument types passed. Any mismatch or
orphaned specifiers/arguments will cause a compile-time error:

-------
import std.format;

void main() {
auto s = format!"%s is %s"("Pi", 3.14);
assert(s == "Pi is 3.14");

static assert(!__traits(compiles, {s = format!"%l"();})); // missing arg
static assert(!__traits(compiles, {s = format!""(404);})); // surplus arg
static assert(!__traits(compiles, {s = format!"%d"(4.03);})); // incompatible arg
}
-------
108 changes: 85 additions & 23 deletions std/format.d
Expand Up @@ -325,20 +325,6 @@ $(I FormatChar):
$(B infinity) if the
$(I FormatChar) is lower case, or $(B INF) or $(B INFINITY) if upper.
Example:
-----------------
import std.array : appender;
import std.format : formattedWrite;
auto writer = appender!string();
formattedWrite(writer, "%s is the ultimate %s.", 42, "answer");
assert(writer.data == "42 is the ultimate answer.");
// Clear the writer
writer = appender!string();
formattedWrite(writer, "Date: %2$s %1$s", "October", 5);
assert(writer.data == "Date: 5 October");
-----------------
The positional and non-positional styles can be mixed in the same
format string. (POSIX leaves this behavior undefined.) The internal
counter for non-positional parameters tracks the next parameter after
Expand Down Expand Up @@ -432,6 +418,31 @@ My friends are "John", "Nancy".
My friends are John, Nancy.
)
*/
uint formattedWrite(alias fmt, Writer, A...)(Writer w, A args)
if (isSomeString!(typeof(fmt)))
{
alias e = checkFormatException!(fmt, A);
static assert(!e, e.msg);
return .formattedWrite(w, fmt, args);
}

/// The format string can be checked at compile-time (see $(LREF format) for details):
@safe pure unittest
{
import std.array : appender;
import std.format : formattedWrite;

auto writer = appender!string();
writer.formattedWrite!"%s is the ultimate %s."(42, "answer");
assert(writer.data == "42 is the ultimate answer.");

// Clear the writer
writer = appender!string();
formattedWrite(writer, "Date: %2$s %1$s", "October", 5);
assert(writer.data == "Date: 5 October");
}

/// ditto
uint formattedWrite(Writer, Char, A...)(Writer w, in Char[] fmt, A args)
{
import std.conv : text, to;
Expand Down Expand Up @@ -570,6 +581,15 @@ matching failure happens.
Throws:
An `Exception` if `S.length == 0` and `fmt` has format specifiers
*/
uint formattedRead(alias fmt, R, S...)(ref R r, auto ref S args)
if (isSomeString!(typeof(fmt)))
{
alias e = checkFormatException!(fmt, S);
static assert(!e, e.msg);
return .formattedRead(r, fmt, args);
}

/// ditto
uint formattedRead(R, Char, S...)(ref R r, const(Char)[] fmt, auto ref S args)
{
import std.typecons : isTuple;
Expand Down Expand Up @@ -631,14 +651,14 @@ uint formattedRead(R, Char, S...)(ref R r, const(Char)[] fmt, auto ref S args)
}
}

///
/// The format string can be checked at compile-time (see $(LREF format) for details):
@safe pure unittest
{
string s = "hello!124:34.5";
string a;
int b;
double c;
formattedRead(s, "%s!%s:%s", a, b, c);
s.formattedRead!"%s!%s:%s"(a, b, c);
assert(a == "hello" && b == 124 && c == 34.5);
}

Expand Down Expand Up @@ -1516,10 +1536,10 @@ if (is(Unqual!Char == Char))

/**
Helper function that returns a $(D FormatSpec) for a single specifier given
in $(D fmt)
in $(D fmt).
Params:
fmt = A format specifier
fmt = A format specifier.
Returns:
A $(D FormatSpec) with the specifier parsed.
Expand Down Expand Up @@ -1933,6 +1953,8 @@ private void formatUnsigned(Writer, T, Char)(Writer w, T arg, const ref FormatSp
assert(result == "9");
}

private enum ctfpMessage = "Cannot format floating point types at compile-time";

/**
Floating-point values are formatted like $(D printf) does.
Expand Down Expand Up @@ -1969,6 +1991,7 @@ if (is(FloatingPointTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
}
enforceFmt(find("fgFGaAeEs", fs.spec).length,
"incompatible format character for floating point type");
enforceFmt(!__ctfe, ctfpMessage);

version (CRuntime_Microsoft)
{
Expand All @@ -1986,7 +2009,7 @@ if (is(FloatingPointTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
return formatValue(w, s, f);
}
else // FIXME:workaroun
else // FIXME:workaround
{
s = s[0 .. f.precision < $ ? f.precision : $];
if (!f.flDash)
Expand Down Expand Up @@ -5456,12 +5479,42 @@ private bool needToSwapEndianess(Char)(const ref FormatSpec!Char f)
assert(stream.data == "C");
}

// Used to check format strings are compatible with argument types
package static const checkFormatException(alias fmt, Args...) =
{
try
.format(fmt, Args.init);
catch (Exception e)
return (e.msg == ctfpMessage) ? null : e;
return null;
}();

/*****************************************************
* Format arguments into a string.
*
* Params: fmt = Format string. For detailed specification, see $(REF formattedWrite, std,_format).
* args = Variadic list of arguments to format into returned string.
* Params: fmt = Format string. For detailed specification, see $(LREF formattedWrite).
* args = Variadic list of arguments to _format into returned string.
*/
typeof(fmt) format(alias fmt, Args...)(Args args)
if (isSomeString!(typeof(fmt)))
{
alias e = checkFormatException!(fmt, Args);
static assert(!e, e.msg);
return .format(fmt, args);
}

/// Type checking can be done when fmt is known at compile-time:
@safe unittest
{
auto s = format!"%s is %s"("Pi", 3.14);
assert(s == "Pi is 3.14");

static assert(!__traits(compiles, {s = format!"%l"();})); // missing arg
static assert(!__traits(compiles, {s = format!""(404);})); // surplus arg
static assert(!__traits(compiles, {s = format!"%d"(4.03);})); // incompatible arg
}

/// ditto
immutable(Char)[] format(Char, Args...)(in Char[] fmt, Args args)
if (isSomeChar!Char)
{
Expand Down Expand Up @@ -5530,6 +5583,15 @@ if (isSomeChar!Char)
* A $(LREF FormatException) if the length of `args` is different
* than the number of format specifiers in `fmt`.
*/
char[] sformat(alias fmt, Args...)(char[] buf, Args args)
if (isSomeString!(typeof(fmt)))
{
alias e = checkFormatException!(fmt, Args);
static assert(!e, e.msg);
return .sformat(buf, fmt, args);
}

/// ditto
char[] sformat(Char, Args...)(char[] buf, in Char[] fmt, Args args)
{
import core.exception : onRangeError;
Expand Down Expand Up @@ -5585,12 +5647,12 @@ char[] sformat(Char, Args...)(char[] buf, in Char[] fmt, Args args)
return buf[0 .. i];
}

///
/// The format string can be checked at compile-time (see $(LREF format) for details):
@system unittest
{
char[10] buf;

assert(sformat(buf[], "foo%s", 'C') == "fooC");
assert(buf[].sformat!"foo%s"('C') == "fooC");
assert(sformat(buf[], "%s foo", "bar") == "bar foo");
}

Expand Down

0 comments on commit 4a1733e

Please sign in to comment.