diff options
Diffstat (limited to 'lib/pure/sugar.nim')
-rw-r--r-- | lib/pure/sugar.nim | 429 |
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) |