base parseEnum on a case statement, fixes #14030 (#14046)

* base `parseEnum` on a case statement, fixes #14030

* apply simplifactions / clean up, remove `norm` node, use strVal

* export `normalize` in json.nim

* cmp using nimIdentNormalize, error at CT if ambiguous enum found

`nimIdentNormalize` provided by @cooldome.

We track all names of the branches we have created so far and error if
a duplicate is found.

Dummy change to make github react...

* fix docstring of `nimIdentNormalize`

* make `typ` arg `typedesc`, add lineinfo, call norm. only once
This commit is contained in:
Vindaar 2020-04-22 10:41:56 +02:00 • committed by GitHub
commit d42c5a575d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 11 deletions

View file

@ -158,7 +158,7 @@ export
export export
parsejson.JsonEventKind, parsejson.JsonError, JsonParser, JsonKindError, parsejson.JsonEventKind, parsejson.JsonError, JsonParser, JsonKindError,
open, close, str, getInt, getFloat, kind, getColumn, getLine, getFilename, open, close, str, getInt, getFloat, kind, getColumn, getLine, getFilename,
errorMsg, errorMsgExpected, next, JsonParsingError, raiseParseErr errorMsg, errorMsgExpected, next, JsonParsingError, raiseParseErr, nimIdentNormalize
type type
JsonNodeKind* = enum ## possible JSON node types JsonNodeKind* = enum ## possible JSON node types

View file

@ -75,6 +75,7 @@
import parseutils import parseutils
from math import pow, floor, log10 from math import pow, floor, log10
from algorithm import reverse from algorithm import reverse
import macros # for `parseEnum`
when defined(nimVmExportFixed): when defined(nimVmExportFixed):
from unicode import toLower, toUpper from unicode import toLower, toUpper
@ -278,6 +279,26 @@ proc capitalizeAscii*(s: string): string {.noSideEffect, procvar,
if s.len == 0: result = "" if s.len == 0: result = ""
else: result = toUpperAscii(s[0]) & substr(s, 1) else: result = toUpperAscii(s[0]) & substr(s, 1)
proc nimIdentNormalize*(s: string): string =
## Normalizes the string `s` as a Nim identifier.
##
## That means to convert to lower case and remove any '_' on all characters
## except first one.
runnableExamples:
doAssert nimIdentNormalize("Foo_bar") == "Foobar"
result = newString(s.len)
if s.len > 0:
result[0] = s[0]
var j = 1
for i in 1..len(s) - 1:
if s[i] in {'A'..'Z'}:
result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
inc j
elif s[i] != '_':
result[j] = s[i]
inc j
if j != s.len: setLen(result, j)
proc normalize*(s: string): string {.noSideEffect, procvar, proc normalize*(s: string): string {.noSideEffect, procvar,
rtl, extern: "nsuNormalize".} = rtl, extern: "nsuNormalize".} =
## Normalizes the string `s`. ## Normalizes the string `s`.
@ -1229,8 +1250,65 @@ proc parseBool*(s: string): bool =
of "n", "no", "false", "0", "off": result = false of "n", "no", "false", "0", "off": result = false
else: raise newException(ValueError, "cannot interpret as a bool: " & s) else: raise newException(ValueError, "cannot interpret as a bool: " & s)
proc addOfBranch(s: string, field, enumType: NimNode): NimNode =
result = nnkOfBranch.newTree(
newLit s,
nnkCall.newTree(enumType, field) # `T(<fieldValue>)`
)
macro genEnumStmt(typ: typedesc, argSym: typed, default: typed): untyped =
# generates a case stmt, which assigns the correct enum field given
# a normalized string comparison to the `argSym` input.
# NOTE: for an enum with fields Foo, Bar, ... we cannot generate
# `of "Foo".nimIdentNormalize: Foo`.
# This will fail, if the enum is not defined at top level (e.g. in a block).
# Thus we check for the field value of the (possible holed enum) and convert
# the integer value to the generic argument `typ`.
let typ = typ.getTypeInst[1]
let impl = typ.getImpl[2]
expectKind impl, nnkEnumTy
result = nnkCaseStmt.newTree(nnkDotExpr.newTree(argSym,
bindSym"nimIdentNormalize"))
# stores all processed field strings to give error msg for ambiguous enums
var foundFields: seq[string]
var fStr: string # string of current field
var fNum: BiggestInt # int value of current field
for f in impl:
case f.kind
of nnkEmpty: continue # skip first node of `enumTy`
of nnkSym, nnkIdent: fStr = f.strVal
of nnkEnumFieldDef:
case f[1].kind
of nnkStrLit: fStr = f[1].strVal
of nnkTupleConstr:
fStr = f[1][1].strVal
fNum = f[1][0].intVal
of nnkIntLit:
fStr = f[0].strVal
fNum = f[1].intVal
else: error("Invalid tuple syntax!", f[1])
else: error("Invalid node for enum type!", f)
# add field if string not already added
fStr = nimIdentNormalize(fStr)
if fStr notin foundFields:
result.add addOfBranch(fStr, newLit fNum, typ)
foundFields.add fStr
else:
error("Ambiguous enums cannot be parsed, field " & $fStr &
" appears multiple times!")
inc fNum
# finally add else branch to raise or use default
if default == nil:
let raiseStmt = quote do:
raise newException(ValueError, "Invalid enum value: " & $`argSym`)
result.add nnkElse.newTree(raiseStmt)
else:
expectKind(default, nnkSym)
result.add nnkElse.newTree(default)
proc parseEnum*[T: enum](s: string): T = proc parseEnum*[T: enum](s: string): T =
## Parses an enum ``T``. ## Parses an enum ``T``. This errors at compile time, if the given enum
## type contains multiple fields with the same string value.
## ##
## Raises ``ValueError`` for an invalid value in `s`. The comparison is ## Raises ``ValueError`` for an invalid value in `s`. The comparison is
## done in a style insensitive way. ## done in a style insensitive way.
@ -1246,13 +1324,11 @@ proc parseEnum*[T: enum](s: string): T =
doAssertRaises(ValueError): doAssertRaises(ValueError):
echo parseEnum[MyEnum]("third") echo parseEnum[MyEnum]("third")
for e in low(T)..high(T): genEnumStmt(T, s, default = nil)
if cmpIgnoreStyle(s, $e) == 0:
return e
raise newException(ValueError, "invalid enum value: " & s)
proc parseEnum*[T: enum](s: string, default: T): T = proc parseEnum*[T: enum](s: string, default: T): T =
## Parses an enum ``T``. ## Parses an enum ``T``. This errors at compile time, if the given enum
## type contains multiple fields with the same string value.
## ##
## Uses `default` for an invalid value in `s`. The comparison is done in a ## Uses `default` for an invalid value in `s`. The comparison is done in a
## style insensitive way. ## style insensitive way.
@ -1267,10 +1343,7 @@ proc parseEnum*[T: enum](s: string, default: T): T =
doAssert parseEnum[MyEnum]("second") == second doAssert parseEnum[MyEnum]("second") == second
doAssert parseEnum[MyEnum]("last", third) == third doAssert parseEnum[MyEnum]("last", third) == third
for e in low(T)..high(T): genEnumStmt(T, s, default)
if cmpIgnoreStyle(s, $e) == 0:
return e
result = default
proc repeat*(c: char, count: Natural): string {.noSideEffect, proc repeat*(c: char, count: Natural): string {.noSideEffect,
rtl, extern: "nsuRepeatChar".} = rtl, extern: "nsuRepeatChar".} =

View file

@ -348,3 +348,90 @@ when true:
main() main()
#OUT ha/home/a1xyz/usr/bin #OUT ha/home/a1xyz/usr/bin
# `parseEnum`, ref issue #14030
# check enum defined at top level
type
Foo = enum
A
B = "bb"
C = (5, "ccc")
D = 15
E = "ee" # check that we count enum fields correctly
block:
let a = parseEnum[Foo]("A")
let b = parseEnum[Foo]("bb")
let c = parseEnum[Foo]("ccc")
let d = parseEnum[Foo]("D")
let e = parseEnum[Foo]("ee")
doAssert a == A
doAssert b == B
doAssert c == C
doAssert d == D
doAssert e == E
try:
let f = parseEnum[Foo]("Bar")
doAssert false
except ValueError:
discard
# finally using default
let g = parseEnum[Foo]("Bar", A)
doAssert g == A
block:
# check enum defined in block
type
Bar = enum
V
W = "ww"
X = (3, "xx")
Y = 10
Z = "zz" # check that we count enum fields correctly
let a = parseEnum[Bar]("V")
let b = parseEnum[Bar]("ww")
let c = parseEnum[Bar]("xx")
let d = parseEnum[Bar]("Y")
let e = parseEnum[Bar]("zz")
doAssert a == V
doAssert b == W
doAssert c == X
doAssert d == Y
doAssert e == Z
try:
let f = parseEnum[Bar]("Baz")
doAssert false
except ValueError:
discard
# finally using default
let g = parseEnum[Bar]("Baz", V)
doAssert g == V
block:
# check ambiguous enum fails to parse
type
Ambig = enum
f1 = "A"
f2 = "B"
f3 = "A"
doAssert not compiles((let a = parseEnum[Ambig]("A")))
block:
# check almost ambiguous enum
type
AlmostAmbig = enum
f1 = "someA"
f2 = "someB"
f3 = "SomeA"
let a = parseEnum[AlmostAmbig]("someA")
let b = parseEnum[AlmostAmbig]("someB")
let c = parseEnum[AlmostAmbig]("SomeA")
doAssert a == f1
doAssert b == f2
doAssert c == f3