Files
Nim/lib/pure/sugar.nim
metagn 770f8d5513 opensym for templates + move behavior of opensymchoice to itself (#24007)
fixes #15314, fixes #24002

The OpenSym behavior first added to generics in #23091 now also applies
to templates, since templates can also capture symbols that are meant to
be replaced by local symbols if the context imports symbols with the
same name, as in the issue #24002. The experimental switch
`templateOpenSym` is added to enable this behavior for templates only,
and the experimental switch `openSym` is added to enable it for both
templates and generics, and the documentation now mainly mentions this
switch.

Additionally the logic for `nkOpenSymChoice` nodes that were previously
wrapped in `nkOpenSym` now apply to all `nkOpenSymChoice` nodes, and so
these nodes aren't wrapped in `nkOpenSym` anymore. This means
`nkOpenSym` can only have children of kind `nkSym` again, so it is more
in line with the structure of symchoice nodes. As for why they aren't
merged with `nkOpenSymChoice` nodes yet, we need some way to signal that
the node shouldn't become ambiguous if other options exist at
instantiation time, we already captured a symbol at the beginning and
another symbol can only replace it if it's closer in scope and
unambiguous.
2024-08-28 20:51:13 +02:00

430 lines
13 KiB
Nim

#
#
# 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)