Strformat symbol binding (#10927)

This commit is contained in:
Arne Döring 2019-04-05 15:27:04 +02:00 • committed by Andreas Rumpf
commit 3a5a0f6d46
6 changed files with 186 additions and 138 deletions

View file

@ -88,48 +88,21 @@ An expression like ``&"{key} is {value:arg} {{z}}"`` is transformed into:
.. code-block:: nim
var temp = newStringOfCap(educatedCapGuess)
format(key, temp)
format(" is ", temp)
format(value, arg, temp)
format(" {z}", temp)
temp.formatValue key, ""
temp.add " is "
temp.formatValue value, arg
temp.add " {z}"
temp
Parts of the string that are enclosed in the curly braces are interpreted
as Nim code, to escape an ``{`` or ``}`` double it.
``&`` delegates most of the work to an open overloaded set
of ``format`` procs. The required signature for a type ``T`` that supports
formatting is usually ``proc format(x: T; result: var string)`` for efficiency
but can also be ``proc format(x: T): string``. ``add`` and ``$`` procs are
used as the fallback implementation.
This is the concrete lookup algorithm that ``&`` uses:
.. code-block:: nim
when compiles(format(arg, res)):
format(arg, res)
elif compiles(format(arg)):
res.add format(arg)
elif compiles(add(res, arg)):
res.add(arg)
else:
res.add($arg)
of ``formatValue`` procs. The required signature for a type ``T`` that supports
formatting is usually ``proc formatValue(result: var string; x: T; specifier: string)``.
The subexpression after the colon
(``arg`` in ``&"{key} is {value:arg} {{z}}"``) is an optional argument
passed to ``format``.
If an optional argument is present the following lookup algorithm is used:
.. code-block:: nim
when compiles(format(arg, option, res)):
format(arg, option, res)
else:
res.add format(arg, option)
(``arg`` in ``&"{key} is {value:arg} {{z}}"``) is optional. It will be passed as the last argument to ``formatValue``. When the colon with the subexpression it is left out, an empty string will be taken instead.
For strings and numeric types the optional argument is a so-called
"standard format specifier".
@ -242,7 +215,7 @@ Future directions
=================
A curly expression with commas in it like ``{x, argA, argB}`` could be
transformed to ``format(x, argA, argB, res)`` in order to support
transformed to ``formatValue(result, x, argA, argB)`` in order to support
formatters that do not need to parse a custom language within a custom
language but instead prefer to use Nim's existing syntax. This also
helps in readability since there is only so much you can cram into
@ -251,96 +224,7 @@ single letter DSLs.
]##
import macros, parseutils, unicode
import strutils
template callFormat(res, arg) {.dirty.} =
when arg is string:
# workaround in order to circumvent 'strutils.format' which matches
# too but doesn't adhere to our protocol.
res.add arg
elif compiles(format(arg, res)) and
# Check if format returns void
not (compiles do: discard format(arg, res)):
format(arg, res)
elif compiles(format(arg)):
res.add format(arg)
elif compiles(add(res, arg)):
res.add(arg)
else:
res.add($arg)
template callFormatOption(res, arg, option) {.dirty.} =
when compiles(format(arg, option, res)):
format(arg, option, res)
elif compiles(format(arg, option)):
res.add format(arg, option)
else:
format($arg, option, res)
macro `&`*(pattern: string): untyped =
## For a specification of the ``&`` macro, see the module level documentation.
if pattern.kind notin {nnkStrLit..nnkTripleStrLit}:
error "string formatting (fmt(), &) only works with string literals", pattern
let f = pattern.strVal
var i = 0
let res = genSym(nskVar, "fmtRes")
result = newNimNode(nnkStmtListExpr, lineInfoFrom=pattern)
# XXX: https://github.com/nim-lang/Nim/issues/8405
# When compiling with -d:useNimRtl, certain procs such as `count` from the strutils
# module are not accessible at compile-time:
let expectedGrowth = when defined(useNimRtl): 0 else: count(f, '{') * 10
result.add newVarStmt(res, newCall(bindSym"newStringOfCap", newLit(f.len + expectedGrowth)))
var strlit = ""
while i < f.len:
if f[i] == '{':
inc i
if f[i] == '{':
inc i
strlit.add '{'
else:
if strlit.len > 0:
result.add newCall(bindSym"add", res, newLit(strlit))
strlit = ""
var subexpr = ""
while i < f.len and f[i] != '}' and f[i] != ':':
subexpr.add f[i]
inc i
let x = parseExpr(subexpr)
if f[i] == ':':
inc i
var options = ""
while i < f.len and f[i] != '}':
options.add f[i]
inc i
result.add getAst(callFormatOption(res, x, newLit(options)))
else:
result.add getAst(callFormat(res, x))
if f[i] == '}':
inc i
else:
doAssert false, "invalid format string: missing '}'"
elif f[i] == '}':
if f[i+1] == '}':
strlit.add '}'
inc i, 2
else:
doAssert false, "invalid format string: '}' instead of '}}'"
inc i
else:
strlit.add f[i]
inc i
if strlit.len > 0:
result.add newCall(bindSym"add", res, newLit(strlit))
result.add res
when defined(debugFmtDsl):
echo repr result
template fmt*(pattern: string): untyped =
## An alias for ``&``.
bind `&`
&pattern
import strutils except format
proc mkDigit(v: int, typ: char): string {.inline.} =
assert(v < 26)
@ -491,8 +375,7 @@ proc parseStandardFormatSpecifier*(s: string; start = 0;
raise newException(ValueError,
"invalid format string, cannot parse: " & s[i..^1])
proc format*(value: SomeInteger; specifier: string; res: var string) =
proc formatValue*(result: var string; value: SomeInteger; specifier: string) =
## Standard format implementation for ``SomeInteger``. It makes little
## sense to call this directly, but it is required to exist
## by the ``&`` macro.
@ -507,9 +390,9 @@ proc format*(value: SomeInteger; specifier: string; res: var string) =
raise newException(ValueError,
"invalid type in format string for number, expected one " &
" of 'x', 'X', 'b', 'd', 'o' but got: " & spec.typ)
res.add formatInt(value, radix, spec)
result.add formatInt(value, radix, spec)
proc format*(value: SomeFloat; specifier: string; res: var string) =
proc formatValue*(result: var string; value: SomeFloat; specifier: string): void =
## Standard format implementation for ``SomeFloat``. It makes little
## sense to call this directly, but it is required to exist
## by the ``&`` macro.
@ -557,14 +440,13 @@ proc format*(value: SomeFloat; specifier: string; res: var string) =
# the default for numbers is right-alignment:
let align = if spec.align == '\0': '>' else: spec.align
let result = alignString(f, spec.minimumWidth,
align, spec.fill)
let res = alignString(f, spec.minimumWidth, align, spec.fill)
if spec.typ in {'A'..'Z'}:
res.add toUpperAscii(result)
result.add toUpperAscii(res)
else:
res.add result
result.add res
proc format*(value: string; specifier: string; res: var string) =
proc formatValue*(result: var string; value: string; specifier: string) =
## Standard format implementation for ``string``. It makes little
## sense to call this directly, but it is required to exist
## by the ``&`` macro.
@ -579,7 +461,92 @@ proc format*(value: string; specifier: string; res: var string) =
if spec.precision != -1:
if spec.precision < runelen(value):
setLen(value, runeOffset(value, spec.precision))
res.add alignString(value, spec.minimumWidth, spec.align, spec.fill)
result.add alignString(value, spec.minimumWidth, spec.align, spec.fill)
template formatValue[T: enum](result: var string; value: T; specifier: string) =
result.add $value
template formatValue(result: var string; value: char; specifier: string) =
result.add value
template formatValue(result: var string; value: cstring; specifier: string) =
result.add value
proc formatValue[T](result: var string; value: openarray[T]; specifier: string) =
result.add "["
for i, it in value:
if i != 0:
result.add ", "
result.formatValue(it, specifier)
result.add "]"
macro `&`*(pattern: string): untyped =
## For a specification of the ``&`` macro, see the module level documentation.
if pattern.kind notin {nnkStrLit..nnkTripleStrLit}:
error "string formatting (fmt(), &) only works with string literals", pattern
let f = pattern.strVal
var i = 0
let res = genSym(nskVar, "fmtRes")
result = newNimNode(nnkStmtListExpr, lineInfoFrom=pattern)
# XXX: https://github.com/nim-lang/Nim/issues/8405
# When compiling with -d:useNimRtl, certain procs such as `count` from the strutils
# module are not accessible at compile-time:
let expectedGrowth = when defined(useNimRtl): 0 else: count(f, '{') * 10
result.add newVarStmt(res, newCall(bindSym"newStringOfCap", newLit(f.len + expectedGrowth)))
var strlit = ""
while i < f.len:
if f[i] == '{':
inc i
if f[i] == '{':
inc i
strlit.add '{'
else:
if strlit.len > 0:
result.add newCall(bindSym"add", res, newLit(strlit))
strlit = ""
var subexpr = ""
while i < f.len and f[i] != '}' and f[i] != ':':
subexpr.add f[i]
inc i
var x: NimNode
try:
x = parseExpr(subexpr)
except ValueError:
let msg = getCurrentExceptionMsg()
error("could not parse ``" & subexpr & "``.\n" & msg, pattern)
let formatSym = bindSym("formatValue", brOpen)
var options = ""
if f[i] == ':':
inc i
while i < f.len and f[i] != '}':
options.add f[i]
inc i
if f[i] == '}':
inc i
else:
doAssert false, "invalid format string: missing '}'"
result.add newCall(formatSym, res, x, newLit(options))
elif f[i] == '}':
if f[i+1] == '}':
strlit.add '}'
inc i, 2
else:
doAssert false, "invalid format string: '}' instead of '}}'"
inc i
else:
strlit.add f[i]
inc i
if strlit.len > 0:
result.add newCall(bindSym"add", res, newLit(strlit))
result.add res
when defined(debugFmtDsl):
echo repr result
template fmt*(pattern: string): untyped =
## An alias for ``&``.
bind `&`
&pattern
when isMainModule:
template check(actual, expected: string) =