summary refs log tree commit diff stats
path: root/lib/pure/sugar.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/sugar.nim')
-rw-r--r--lib/pure/sugar.nim429
1 files changed, 429 insertions, 0 deletions
diff --git a/lib/pure/sugar.nim b/lib/pure/sugar.nim
new file mode 100644
index 000000000..90ba20c13
--- /dev/null
+++ b/lib/pure/sugar.nim
@@ -0,0 +1,429 @@
+#
+#
+#            Nim's Runtime Library
+#        (c) Copyright 2015 Dominik Picheta
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## This module implements nice syntactic sugar based on Nim's
+## macro system.
+
+import std/private/since
+import std/macros
+
+proc checkPragma(ex, prag: var NimNode) =
+  since (1, 3):
+    if ex.kind == nnkPragmaExpr:
+      prag = ex[1]
+      ex = ex[0]
+
+proc createProcType(p, b: NimNode): NimNode =
+  result = newNimNode(nnkProcTy)
+  var
+    formalParams = newNimNode(nnkFormalParams).add(b)
+    p = p
+    prag = newEmptyNode()
+
+  checkPragma(p, prag)
+
+  case p.kind
+  of nnkPar, nnkTupleConstr:
+    for i in 0 ..< p.len:
+      let ident = p[i]
+      var identDefs = newNimNode(nnkIdentDefs)
+      case ident.kind
+      of nnkExprColonExpr:
+        identDefs.add ident[0]
+        identDefs.add ident[1]
+      else:
+        identDefs.add newIdentNode("i" & $i)
+        identDefs.add(ident)
+      identDefs.add newEmptyNode()
+      formalParams.add identDefs
+  else:
+    var identDefs = newNimNode(nnkIdentDefs)
+    identDefs.add newIdentNode("i0")
+    identDefs.add(p)
+    identDefs.add newEmptyNode()
+    formalParams.add identDefs
+
+  result.add formalParams
+  result.add prag
+
+macro `=>`*(p, b: untyped): untyped =
+  ## Syntax sugar for anonymous procedures. It also supports pragmas.
+  ##
+  ## .. warning:: Semicolons can not be used to separate procedure arguments.
+  runnableExamples:
+    proc passTwoAndTwo(f: (int, int) -> int): int = f(2, 2)
+
+    assert passTwoAndTwo((x, y) => x + y) == 4
+
+    type
+      Bot = object
+        call: (string {.noSideEffect.} -> string)
+
+    var myBot = Bot()
+
+    myBot.call = (name: string) {.noSideEffect.} => "Hello " & name & ", I'm a bot."
+    assert myBot.call("John") == "Hello John, I'm a bot."
+
+    let f = () => (discard) # simplest proc that returns void
+    f()
+
+  var
+    params = @[ident"auto"]
+    name = newEmptyNode()
+    kind = nnkLambda
+    pragma = newEmptyNode()
+    p = p
+
+  checkPragma(p, pragma)
+
+  if p.kind == nnkInfix and p[0].kind == nnkIdent and p[0].eqIdent"->":
+    params[0] = p[2]
+    p = p[1]
+
+  checkPragma(p, pragma) # check again after -> transform
+
+  case p.kind
+  of nnkPar, nnkTupleConstr:
+    var untypedBeforeColon = 0
+    for i, c in p:
+      var identDefs = newNimNode(nnkIdentDefs)
+      case c.kind
+      of nnkExprColonExpr:
+        let t = c[1]
+        since (1, 3):
+          # + 1 here because of return type in params
+          for j in (i - untypedBeforeColon + 1) .. i:
+            params[j][1] = t
+        untypedBeforeColon = 0
+        identDefs.add(c[0])
+        identDefs.add(t)
+        identDefs.add(newEmptyNode())
+      of nnkIdent:
+        identDefs.add(c)
+        identDefs.add(newIdentNode("auto"))
+        identDefs.add(newEmptyNode())
+        inc untypedBeforeColon
+      of nnkInfix:
+        if c[0].kind == nnkIdent and c[0].eqIdent"->":
+          var procTy = createProcType(c[1], c[2])
+          params[0] = procTy[0][0]
+          for i in 1 ..< procTy[0].len:
+            params.add(procTy[0][i])
+        else:
+          error("Expected proc type (->) got (" & c[0].strVal & ").", c)
+        break
+      else:
+        error("Incorrect procedure parameter.", c)
+      params.add(identDefs)
+  of nnkIdent, nnkOpenSymChoice, nnkClosedSymChoice, nnkSym:
+    var identDefs = newNimNode(nnkIdentDefs)
+    identDefs.add(ident $p)
+    identDefs.add(ident"auto")
+    identDefs.add(newEmptyNode())
+    params.add(identDefs)
+  else:
+    error("Incorrect procedure parameter list.", p)
+  result = newProc(body = b, params = params,
+                   pragmas = pragma, name = name,
+                   procType = kind)
+
+macro `->`*(p, b: untyped): untyped =
+  ## Syntax sugar for procedure types. It also supports pragmas.
+  ##
+  ## .. warning:: Semicolons can not be used to separate procedure arguments.
+  runnableExamples:
+    proc passTwoAndTwo(f: (int, int) -> int): int = f(2, 2)
+    # is the same as:
+    # proc passTwoAndTwo(f: proc (x, y: int): int): int = f(2, 2)
+
+    assert passTwoAndTwo((x, y) => x + y) == 4
+
+    proc passOne(f: (int {.noSideEffect.} -> int)): int = f(1)
+    # is the same as:
+    # proc passOne(f: proc (x: int): int {.noSideEffect.}): int = f(1)
+
+    assert passOne(x {.noSideEffect.} => x + 1) == 2
+
+  result = createProcType(p, b)
+
+macro dump*(x: untyped): untyped =
+  ## Dumps the content of an expression, useful for debugging.
+  ## It accepts any expression and prints a textual representation
+  ## of the tree representing the expression - as it would appear in
+  ## source code - together with the value of the expression.
+  ##
+  ## See also: `dumpToString` which is more convenient and useful since
+  ## it expands intermediate templates/macros, returns a string instead of
+  ## calling `echo`, and works with statements and expressions.
+  runnableExamples("-r:off"):
+    let
+      x = 10
+      y = 20
+    dump(x + y) # prints: `x + y = 30`
+
+  let s = x.toStrLit
+  result = quote do:
+    debugEcho `s`, " = ", `x`
+
+macro dumpToStringImpl(s: static string, x: typed): string =
+  let s2 = x.toStrLit
+  if x.typeKind == ntyVoid:
+    result = quote do:
+      `s` & ": " & `s2`
+  else:
+    result = quote do:
+      `s` & ": " & `s2` & " = " & $`x`
+
+macro dumpToString*(x: untyped): string =
+  ## Returns the content of a statement or expression `x` after semantic analysis,
+  ## useful for debugging.
+  runnableExamples:
+    const a = 1
+    let x = 10
+    assert dumpToString(a + 2) == "a + 2: 3 = 3"
+    assert dumpToString(a + x) == "a + x: 1 + x = 11"
+    template square(x): untyped = x * x
+    assert dumpToString(square(x)) == "square(x): x * x = 100"
+    assert not compiles dumpToString(1 + nonexistent)
+    import std/strutils
+    assert "failedAssertImpl" in dumpToString(assert true) # example with a statement
+  result = newCall(bindSym"dumpToStringImpl")
+  result.add newLit repr(x)
+  result.add x
+
+# TODO: consider exporting this in macros.nim
+proc freshIdentNodes(ast: NimNode): NimNode =
+  # Replace NimIdent and NimSym by a fresh ident node
+  # see also https://github.com/nim-lang/Nim/pull/8531#issuecomment-410436458
+  proc inspect(node: NimNode): NimNode =
+    case node.kind:
+    of nnkIdent, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice, nnkOpenSym:
+      result = ident($node)
+    of nnkEmpty, nnkLiterals:
+      result = node
+    else:
+      result = node.kind.newTree()
+      for child in node:
+        result.add inspect(child)
+  result = inspect(ast)
+
+macro capture*(locals: varargs[typed], body: untyped): untyped {.since: (1, 1).} =
+  ## Useful when creating a closure in a loop to capture some local loop variables
+  ## by their current iteration values.
+  runnableExamples:
+    import std/strformat
+
+    var myClosure: () -> string
+    for i in 5..7:
+      for j in 7..9:
+        if i * j == 42:
+          capture i, j:
+            myClosure = () => fmt"{i} * {j} = 42"
+    assert myClosure() == "6 * 7 = 42"
+
+  var params = @[newIdentNode("auto")]
+  let locals = if locals.len == 1 and locals[0].kind == nnkBracket: locals[0]
+               else: locals
+  for arg in locals:
+    proc getIdent(n: NimNode): NimNode =
+      case n.kind
+      of nnkIdent, nnkSym:
+        let nStr = n.strVal
+        if nStr == "result":
+          error("The variable name cannot be `result`!", n)
+        result = ident(nStr)
+      of nnkHiddenDeref: result = n[0].getIdent()
+      else:
+        error("The argument to be captured `" & n.repr & "` is not a pure identifier. " &
+          "It is an unsupported `" & $n.kind & "` node.", n)
+    let argName = getIdent(arg)
+    params.add(newIdentDefs(argName, freshIdentNodes getTypeInst arg))
+  result = newNimNode(nnkCall)
+  result.add(newProc(newEmptyNode(), params, body, nnkLambda))
+  for arg in locals: result.add(arg)
+
+since (1, 1):
+  import std/private/underscored_calls
+
+  macro dup*[T](arg: T, calls: varargs[untyped]): T =
+    ## Turns an `in-place`:idx: algorithm into one that works on
+    ## a copy and returns this copy, without modifying its input.
+    ##
+    ## This macro also allows for (otherwise in-place) function chaining.
+    ##
+    ## **Since:** Version 1.2.
+    runnableExamples:
+      import std/algorithm
+
+      let a = @[1, 2, 3, 4, 5, 6, 7, 8, 9]
+      assert a.dup(sort) == sorted(a)
+
+      # Chaining:
+      var aCopy = a
+      aCopy.insert(10)
+      assert a.dup(insert(10), sort) == sorted(aCopy)
+
+      let s1 = "abc"
+      let s2 = "xyz"
+      assert s1 & s2 == s1.dup(&= s2)
+
+      # An underscore (_) can be used to denote the place of the argument you're passing:
+      assert "".dup(addQuoted(_, "foo")) == "\"foo\""
+      # but `_` is optional here since the substitution is in 1st position:
+      assert "".dup(addQuoted("foo")) == "\"foo\""
+
+      proc makePalindrome(s: var string) =
+        for i in countdown(s.len-2, 0):
+          s.add(s[i])
+
+      let c = "xyz"
+
+      # chaining:
+      let d = dup c:
+        makePalindrome # xyzyx
+        sort(_, SortOrder.Descending) # zyyxx
+        makePalindrome # zyyxxxyyz
+      assert d == "zyyxxxyyz"
+
+    result = newNimNode(nnkStmtListExpr, arg)
+    let tmp = genSym(nskVar, "dupResult")
+    result.add newVarStmt(tmp, arg)
+    underscoredCalls(result, calls, tmp)
+    result.add tmp
+
+proc trans(n, res, bracketExpr: NimNode): (NimNode, NimNode, NimNode) {.since: (1, 1).} =
+  # Looks for the last statement of the last statement, etc...
+  case n.kind
+  of nnkIfExpr, nnkIfStmt, nnkTryStmt, nnkCaseStmt, nnkWhenStmt:
+    result[0] = copyNimTree(n)
+    result[1] = copyNimTree(n)
+    result[2] = copyNimTree(n)
+    for i in ord(n.kind == nnkCaseStmt) ..< n.len:
+      (result[0][i], result[1][^1], result[2][^1]) = trans(n[i], res, bracketExpr)
+  of nnkStmtList, nnkStmtListExpr, nnkBlockStmt, nnkBlockExpr, nnkWhileStmt,
+      nnkForStmt, nnkElifBranch, nnkElse, nnkElifExpr, nnkOfBranch, nnkExceptBranch:
+    result[0] = copyNimTree(n)
+    result[1] = copyNimTree(n)
+    result[2] = copyNimTree(n)
+    if n.len >= 1:
+      (result[0][^1], result[1][^1], result[2][^1]) = trans(n[^1],
+          res, bracketExpr)
+  of nnkTableConstr:
+    result[1] = n[0][0]
+    result[2] = n[0][1]
+    if bracketExpr.len == 0:
+      bracketExpr.add(ident"initTable") # don't import tables
+    if bracketExpr.len == 1:
+      bracketExpr.add([newCall(bindSym"typeof",
+          newEmptyNode()), newCall(bindSym"typeof", newEmptyNode())])
+    template adder(res, k, v) = res[k] = v
+    result[0] = getAst(adder(res, n[0][0], n[0][1]))
+  of nnkCurly:
+    result[2] = n[0]
+    if bracketExpr.len == 0:
+      bracketExpr.add(ident"initHashSet")
+    if bracketExpr.len == 1:
+      bracketExpr.add(newCall(bindSym"typeof", newEmptyNode()))
+    template adder(res, v) = res.incl(v)
+    result[0] = getAst(adder(res, n[0]))
+  else:
+    result[2] = n
+    if bracketExpr.len == 0:
+      bracketExpr.add(bindSym"newSeq")
+    if bracketExpr.len == 1:
+      bracketExpr.add(newCall(bindSym"typeof", newEmptyNode()))
+    template adder(res, v) = res.add(v)
+    result[0] = getAst(adder(res, n))
+
+proc collectImpl(init, body: NimNode): NimNode {.since: (1, 1).} =
+  let res = genSym(nskVar, "collectResult")
+  var bracketExpr: NimNode
+  if init != nil:
+    expectKind init, {nnkCall, nnkIdent, nnkSym, nnkClosedSymChoice, nnkOpenSymChoice, nnkOpenSym}
+    bracketExpr = newTree(nnkBracketExpr,
+      if init.kind in {nnkCall, nnkClosedSymChoice, nnkOpenSymChoice, nnkOpenSym}:
+        freshIdentNodes(init[0]) else: freshIdentNodes(init))
+  else:
+    bracketExpr = newTree(nnkBracketExpr)
+  let (resBody, keyType, valueType) = trans(body, res, bracketExpr)
+  if bracketExpr.len == 3:
+    bracketExpr[1][1] = keyType
+    bracketExpr[2][1] = valueType
+  else:
+    bracketExpr[1][1] = valueType
+  let call = newTree(nnkCall, bracketExpr)
+  if init != nil and init.kind == nnkCall:
+    for i in 1 ..< init.len:
+      call.add init[i]
+  result = newTree(nnkStmtListExpr, newVarStmt(res, call), resBody, res)
+
+macro collect*(init, body: untyped): untyped {.since: (1, 1).} =
+  ## Comprehension for seqs/sets/tables.
+  ##
+  ## The last expression of `body` has special syntax that specifies
+  ## the collection's add operation. Use `{e}` for set's `incl`,
+  ## `{k: v}` for table's `[]=` and `e` for seq's `add`.
+  # analyse the body, find the deepest expression 'it' and replace it via
+  # 'result.add it'
+  runnableExamples:
+    import std/[sets, tables]
+
+    let data = @["bird", "word"]
+
+    ## seq:
+    let k = collect(newSeq):
+      for i, d in data.pairs:
+        if i mod 2 == 0: d
+    assert k == @["bird"]
+
+    ## seq with initialSize:
+    let x = collect(newSeqOfCap(4)):
+      for i, d in data.pairs:
+        if i mod 2 == 0: d
+    assert x == @["bird"]
+
+    ## HashSet:
+    let y = collect(initHashSet()):
+      for d in data.items: {d}
+    assert y == data.toHashSet
+
+    ## Table:
+    let z = collect(initTable(2)):
+      for i, d in data.pairs: {i: d}
+    assert z == {0: "bird", 1: "word"}.toTable
+
+  result = collectImpl(init, body)
+
+macro collect*(body: untyped): untyped {.since: (1, 5).} =
+  ## Same as `collect` but without an `init` parameter.
+  ##
+  ## **See also:**
+  ## * `sequtils.toSeq proc<sequtils.html#toSeq.t%2Cuntyped>`_
+  ## * `sequtils.mapIt template<sequtils.html#mapIt.t%2Ctyped%2Cuntyped>`_
+  runnableExamples:
+    import std/[sets, tables]
+    let data = @["bird", "word"]
+
+    # seq:
+    let k = collect:
+      for i, d in data.pairs:
+        if i mod 2 == 0: d
+    assert k == @["bird"]
+
+    ## HashSet:
+    let n = collect:
+      for d in data.items: {d}
+    assert n == data.toHashSet
+
+    ## Table:
+    let m = collect:
+      for i, d in data.pairs: {i: d}
+    assert m == {0: "bird", 1: "word"}.toTable
+
+  result = collectImpl(nil, body)