enumutils.nim 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2020 Nim contributors
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. import macros
  10. from typetraits import OrdinalEnum, HoleyEnum
  11. # xxx `genEnumCaseStmt` needs tests and runnableExamples
  12. macro genEnumCaseStmt*(typ: typedesc, argSym: typed, default: typed,
  13. userMin, userMax: static[int], normalizer: static[proc(s :string): string]): untyped =
  14. # generates a case stmt, which assigns the correct enum field given
  15. # a normalized string comparison to the `argSym` input.
  16. # string normalization is done using passed normalizer.
  17. # NOTE: for an enum with fields Foo, Bar, ... we cannot generate
  18. # `of "Foo".nimIdentNormalize: Foo`.
  19. # This will fail, if the enum is not defined at top level (e.g. in a block).
  20. # Thus we check for the field value of the (possible holed enum) and convert
  21. # the integer value to the generic argument `typ`.
  22. let typ = typ.getTypeInst[1]
  23. let impl = typ.getImpl[2]
  24. expectKind impl, nnkEnumTy
  25. let normalizerNode = quote: `normalizer`
  26. expectKind normalizerNode, nnkSym
  27. result = nnkCaseStmt.newTree(newCall(normalizerNode, argSym))
  28. # stores all processed field strings to give error msg for ambiguous enums
  29. var foundFields: seq[string] = @[]
  30. var fStr = "" # string of current field
  31. var fNum = BiggestInt(0) # int value of current field
  32. for f in impl:
  33. case f.kind
  34. of nnkEmpty: continue # skip first node of `enumTy`
  35. of nnkSym, nnkIdent: fStr = f.strVal
  36. of nnkAccQuoted:
  37. fStr = ""
  38. for ch in f:
  39. fStr.add ch.strVal
  40. of nnkEnumFieldDef:
  41. case f[1].kind
  42. of nnkStrLit: fStr = f[1].strVal
  43. of nnkTupleConstr:
  44. fStr = f[1][1].strVal
  45. fNum = f[1][0].intVal
  46. of nnkIntLit:
  47. fStr = f[0].strVal
  48. fNum = f[1].intVal
  49. else: error("Invalid tuple syntax!", f[1])
  50. else: error("Invalid node for enum type `" & $f.kind & "`!", f)
  51. # add field if string not already added
  52. if fNum >= userMin and fNum <= userMax:
  53. fStr = normalizer(fStr)
  54. if fStr notin foundFields:
  55. result.add nnkOfBranch.newTree(newLit fStr, nnkCall.newTree(typ, newLit fNum))
  56. foundFields.add fStr
  57. else:
  58. error("Ambiguous enums cannot be parsed, field " & $fStr &
  59. " appears multiple times!", f)
  60. inc fNum
  61. # finally add else branch to raise or use default
  62. if default == nil:
  63. let raiseStmt = quote do:
  64. raise newException(ValueError, "Invalid enum value: " & $`argSym`)
  65. result.add nnkElse.newTree(raiseStmt)
  66. else:
  67. expectKind(default, nnkSym)
  68. result.add nnkElse.newTree(default)
  69. macro enumFullRange(a: typed): untyped =
  70. newNimNode(nnkCurly).add(a.getType[1][1..^1])
  71. macro enumNames(a: typed): untyped =
  72. # this could be exported too; in particular this could be useful for enum with holes.
  73. result = newNimNode(nnkBracket)
  74. for ai in a.getType[1][1..^1]:
  75. assert ai.kind == nnkSym
  76. result.add newLit ai.strVal
  77. iterator items*[T: HoleyEnum](E: typedesc[T]): T =
  78. ## Iterates over an enum with holes.
  79. runnableExamples:
  80. type
  81. A = enum
  82. a0 = 2
  83. a1 = 4
  84. a2
  85. B[T] = enum
  86. b0 = 2
  87. b1 = 4
  88. from std/sequtils import toSeq
  89. assert A.toSeq == [a0, a1, a2]
  90. assert B[float].toSeq == [B[float].b0, B[float].b1]
  91. for a in enumFullRange(E): yield a
  92. func span(T: typedesc[HoleyEnum]): int =
  93. (T.high.ord - T.low.ord) + 1
  94. const invalidSlot = uint8.high
  95. proc genLookup[T: typedesc[HoleyEnum]](_: T): auto =
  96. const n = span(T)
  97. var ret: array[n, uint8]
  98. var i = 0
  99. assert n <= invalidSlot.int
  100. for ai in mitems(ret): ai = invalidSlot
  101. for ai in items(T):
  102. ret[ai.ord - T.low.ord] = uint8(i)
  103. inc(i)
  104. return ret
  105. func symbolRankImpl[T](a: T): int {.inline.} =
  106. const n = T.span
  107. const thres = 255 # must be <= `invalidSlot`, but this should be tuned.
  108. when n <= thres:
  109. const lookup = genLookup(T)
  110. let lookup2 {.global.} = lookup # xxx improve pending https://github.com/timotheecour/Nim/issues/553
  111. #[
  112. This could be optimized using a hash adapted to `T` (possible since it's known at CT)
  113. to get better key distribution before indexing into the lookup table table.
  114. ]#
  115. {.noSideEffect.}: # because it's immutable
  116. let ret = lookup2[ord(a) - T.low.ord]
  117. if ret != invalidSlot: return ret.int
  118. else:
  119. var i = 0
  120. # we could also generate a case statement as optimization
  121. for ai in items(T):
  122. if ai == a: return i
  123. inc(i)
  124. raise newException(IndexDefect, $ord(a) & " invalid for " & $T)
  125. template symbolRank*[T: enum](a: T): int =
  126. ## Returns the index in which `a` is listed in `T`.
  127. ##
  128. ## The cost for a `HoleyEnum` is implementation defined, currently optimized
  129. ## for small enums, otherwise is `O(T.enumLen)`.
  130. runnableExamples:
  131. type
  132. A = enum # HoleyEnum
  133. a0 = -3
  134. a1 = 10
  135. a2
  136. a3 = (20, "f3Alt")
  137. B = enum # OrdinalEnum
  138. b0
  139. b1
  140. b2
  141. C = enum # OrdinalEnum
  142. c0 = 10
  143. c1
  144. c2
  145. assert a2.symbolRank == 2
  146. assert b2.symbolRank == 2
  147. assert c2.symbolRank == 2
  148. assert c2.ord == 12
  149. assert a2.ord == 11
  150. var invalid = 7.A
  151. doAssertRaises(IndexDefect): discard invalid.symbolRank
  152. when T is Ordinal: ord(a) - T.low.ord.static
  153. else: symbolRankImpl(a)
  154. func symbolName*[T: enum](a: T): string =
  155. ## Returns the symbol name of an enum.
  156. ##
  157. ## This uses `symbolRank`.
  158. runnableExamples:
  159. type B = enum
  160. b0 = (10, "kb0")
  161. b1 = "kb1"
  162. b2
  163. let b = B.low
  164. assert b.symbolName == "b0"
  165. assert $b == "kb0"
  166. static: assert B.high.symbolName == "b2"
  167. type C = enum # HoleyEnum
  168. c0 = -3
  169. c1 = 4
  170. c2 = 20
  171. assert c1.symbolName == "c1"
  172. const names = enumNames(T)
  173. names[a.symbolRank]