From f55d6b05474841bd92e685598b41066cf3f1fc6e Mon Sep 17 00:00:00 2001 From: Nick Treleaven Date: Tue, 14 Mar 2017 12:04:14 +0000 Subject: [PATCH 1/3] Add CT-checked format string overloads to std.format * Add overloads for formattedWrite, formattedRead, format, sformat. * Throw FormatError when formatValue is called with floating points. The latter allows `snprintf` to be avoided in CTFE when checking format strings so floating point arguments can be checked. --- std/format.d | 104 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 23 deletions(-) diff --git a/std/format.d b/std/format.d index 66fb0d490ff..26a4f56a32b 100644 --- a/std/format.d +++ b/std/format.d @@ -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 @@ -432,6 +418,30 @@ My friends are "John", "Nancy". My friends are John, Nancy. ) */ +uint formattedWrite(alias fmt, Writer, A...)(Writer w, A args) +{ + 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; @@ -570,6 +580,14 @@ 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) +{ + 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; @@ -631,14 +649,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); } @@ -1516,10 +1534,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. @@ -1933,6 +1951,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. @@ -1969,6 +1989,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) { @@ -1986,7 +2007,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) @@ -5456,12 +5477,41 @@ 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 is 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) +{ + 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) { @@ -5530,6 +5580,14 @@ 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) +{ + 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; @@ -5585,12 +5643,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"); } From c6a7e3ede48bbe03794220717066da74412125b1 Mon Sep 17 00:00:00 2001 From: Nick Treleaven Date: Sat, 18 Mar 2017 10:32:04 +0000 Subject: [PATCH 2/3] Tweaks Add constraints. Fix test for ctfpMessage equality. --- std/format.d | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/std/format.d b/std/format.d index 26a4f56a32b..e27faffd075 100644 --- a/std/format.d +++ b/std/format.d @@ -419,6 +419,7 @@ 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); @@ -581,6 +582,7 @@ 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); @@ -5483,7 +5485,7 @@ package static const checkFormatException(alias fmt, Args...) = try .format(fmt, Args.init); catch (Exception e) - return (e.msg is ctfpMessage) ? null : e; + return (e.msg == ctfpMessage) ? null : e; return null; }(); @@ -5494,6 +5496,7 @@ package static const checkFormatException(alias fmt, Args...) = * 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); @@ -5581,6 +5584,7 @@ if (isSomeChar!Char) * 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); From 1a8d4f6fe19f15f5b84995f7394533b9a0a65c9b Mon Sep 17 00:00:00 2001 From: Nick Treleaven Date: Sat, 18 Mar 2017 10:48:13 +0000 Subject: [PATCH 3/3] Add changelog entry --- changelog/std-format-formattedWrite.dd | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 changelog/std-format-formattedWrite.dd diff --git a/changelog/std-format-formattedWrite.dd b/changelog/std-format-formattedWrite.dd new file mode 100644 index 00000000000..d79cccf3747 --- /dev/null +++ b/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 +} +-------