macros for proc types, macros for types (#13778)

* new minor feature: macros for proc types, to be documented

* Finished the implementation and added tests

* [skip ci] Describe the new custom pragmas in the manual and the changelog

Co-authored-by: Zahary Karadjov <zahary@gmail.com>
This commit is contained in:
Andreas Rumpf 2020-03-31 21:14:05 +02:00 • committed by GitHub
commit 9134bb9cfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 303 additions and 140 deletions

View file

@ -165,6 +165,8 @@ echo f
- `var a {.foo.}: MyType = expr` now lowers to `foo(a, MyType, expr)` for non builtin pragmas, - `var a {.foo.}: MyType = expr` now lowers to `foo(a, MyType, expr)` for non builtin pragmas,
enabling things like lvalue references, see `pragmas.byaddr` enabling things like lvalue references, see `pragmas.byaddr`
- `macro pragmas` can now be used in type sections.
## Language changes ## Language changes
- Unsigned integer operators have been fixed to allow promotion of the first operand. - Unsigned integer operators have been fixed to allow promotion of the first operand.

View file

@ -1597,48 +1597,43 @@ proc transitionToLet*(s: PSym) =
s.bitsize = obj.bitsize s.bitsize = obj.bitsize
s.alignment = obj.alignment s.alignment = obj.alignment
proc shallowCopy*(src: PNode): PNode = template copyNodeImpl(dst, src, processSonsStmt) =
# does not copy its sons, but provides space for them: if src == nil: return
if src == nil: return nil dst = newNode(src.kind)
result = newNode(src.kind) dst.info = src.info
result.info = src.info dst.typ = src.typ
result.typ = src.typ dst.flags = src.flags * PersistentNodeFlags
result.flags = src.flags * PersistentNodeFlags dst.comment = src.comment
result.comment = src.comment
when defined(useNodeIds): when defined(useNodeIds):
if result.id == nodeIdToDebug: if dst.id == nodeIdToDebug:
echo "COMES FROM ", src.id echo "COMES FROM ", src.id
case src.kind case src.kind
of nkCharLit..nkUInt64Lit: result.intVal = src.intVal of nkCharLit..nkUInt64Lit: dst.intVal = src.intVal
of nkFloatLiterals: result.floatVal = src.floatVal of nkFloatLiterals: dst.floatVal = src.floatVal
of nkSym: result.sym = src.sym of nkSym: dst.sym = src.sym
of nkIdent: result.ident = src.ident of nkIdent: dst.ident = src.ident
of nkStrLit..nkTripleStrLit: result.strVal = src.strVal of nkStrLit..nkTripleStrLit: dst.strVal = src.strVal
else: newSeq(result.sons, src.len) else: processSonsStmt
proc shallowCopy*(src: PNode): PNode =
# does not copy its sons, but provides space for them:
copyNodeImpl(result, src):
newSeq(result.sons, src.len)
proc copyTree*(src: PNode): PNode = proc copyTree*(src: PNode): PNode =
# copy a whole syntax tree; performs deep copying # copy a whole syntax tree; performs deep copying
if src == nil: copyNodeImpl(result, src):
return nil
result = newNode(src.kind)
result.info = src.info
result.typ = src.typ
result.flags = src.flags * PersistentNodeFlags
result.comment = src.comment
when defined(useNodeIds):
if result.id == nodeIdToDebug:
echo "COMES FROM ", src.id
case src.kind
of nkCharLit..nkUInt64Lit: result.intVal = src.intVal
of nkFloatLiterals: result.floatVal = src.floatVal
of nkSym: result.sym = src.sym
of nkIdent: result.ident = src.ident
of nkStrLit..nkTripleStrLit: result.strVal = src.strVal
else:
newSeq(result.sons, src.len) newSeq(result.sons, src.len)
for i in 0..<src.len: for i in 0..<src.len:
result[i] = copyTree(src[i]) result[i] = copyTree(src[i])
proc copyTreeWithoutNode*(src, skippedNode: PNode): PNode =
copyNodeImpl(result, src):
result.sons = newSeqOfCap[PNode](src.len)
for n in src.sons:
if n != skippedNode:
result.sons.add copyTreeWithoutNode(n, skippedNode)
proc hasSonWith*(n: PNode, kind: TNodeKind): bool = proc hasSonWith*(n: PNode, kind: TNodeKind): bool =
for i in 0..<n.len: for i in 0..<n.len:
if n[i].kind == kind: if n[i].kind == kind:

View file

@ -1051,6 +1051,68 @@ proc typeSectionTypeName(c: PContext; n: PNode): PNode =
result = n result = n
if result.kind != nkSym: illFormedAst(n, c.config) if result.kind != nkSym: illFormedAst(n, c.config)
proc typeDefLeftSidePass(c: PContext, typeSection: PNode, i: int) =
let typeDef= typeSection[i]
checkSonsLen(typeDef, 3, c.config)
var name = typeDef[0]
var s: PSym
if name.kind == nkDotExpr and typeDef[2].kind == nkObjectTy:
let pkgName = considerQuotedIdent(c, name[0])
let typName = considerQuotedIdent(c, name[1])
let pkg = c.graph.packageSyms.strTableGet(pkgName)
if pkg.isNil or pkg.kind != skPackage:
localError(c.config, name.info, "unknown package name: " & pkgName.s)
else:
let typsym = pkg.tab.strTableGet(typName)
if typsym.isNil:
s = semIdentDef(c, name[1], skType)
onDef(name[1].info, s)
s.typ = newTypeS(tyObject, c)
s.typ.sym = s
s.flags.incl sfForward
pkg.tab.strTableAdd s
addInterfaceDecl(c, s)
elif typsym.kind == skType and sfForward in typsym.flags:
s = typsym
addInterfaceDecl(c, s)
else:
localError(c.config, name.info, typsym.name.s & " is not a type that can be forwarded")
s = typsym
else:
s = semIdentDef(c, name, skType)
onDef(name.info, s)
s.typ = newTypeS(tyForward, c)
s.typ.sym = s # process pragmas:
if name.kind == nkPragmaExpr:
let rewritten = applyTypeSectionPragmas(c, name[1], typeDef)
if rewritten != nil:
typeSection[i] = rewritten
typeDefLeftSidePass(c, typeSection, i)
return
pragma(c, s, name[1], typePragmas)
if sfForward in s.flags:
# check if the symbol already exists:
let pkg = c.module.owner
if not isTopLevel(c) or pkg.isNil:
localError(c.config, name.info, "only top level types in a package can be 'package'")
else:
let typsym = pkg.tab.strTableGet(s.name)
if typsym != nil:
if sfForward notin typsym.flags or sfNoForward notin typsym.flags:
typeCompleted(typsym)
typsym.info = s.info
else:
localError(c.config, name.info, "cannot complete type '" & s.name.s & "' twice; " &
"previous type completion was here: " & c.config$typsym.info)
s = typsym
# add it here, so that recursive types are possible:
if sfGenSym notin s.flags: addInterfaceDecl(c, s)
elif s.owner == nil: s.owner = getCurrOwner(c)
if name.kind == nkPragmaExpr:
typeDef[0][0] = newSymNode(s)
else:
typeDef[0] = newSymNode(s)
proc typeSectionLeftSidePass(c: PContext, n: PNode) = proc typeSectionLeftSidePass(c: PContext, n: PNode) =
# process the symbols on the left side for the whole type section, before # process the symbols on the left side for the whole type section, before
@ -1064,61 +1126,7 @@ proc typeSectionLeftSidePass(c: PContext, n: PNode) =
dec c.inTypeContext dec c.inTypeContext
if a.kind == nkCommentStmt: continue if a.kind == nkCommentStmt: continue
if a.kind != nkTypeDef: illFormedAst(a, c.config) if a.kind != nkTypeDef: illFormedAst(a, c.config)
checkSonsLen(a, 3, c.config) typeDefLeftSidePass(c, n, i)
let name = a[0]
var s: PSym
if name.kind == nkDotExpr and a[2].kind == nkObjectTy:
let pkgName = considerQuotedIdent(c, name[0])
let typName = considerQuotedIdent(c, name[1])
let pkg = c.graph.packageSyms.strTableGet(pkgName)
if pkg.isNil or pkg.kind != skPackage:
localError(c.config, name.info, "unknown package name: " & pkgName.s)
else:
let typsym = pkg.tab.strTableGet(typName)
if typsym.isNil:
s = semIdentDef(c, name[1], skType)
onDef(name[1].info, s)
s.typ = newTypeS(tyObject, c)
s.typ.sym = s
s.flags.incl sfForward
pkg.tab.strTableAdd s
addInterfaceDecl(c, s)
elif typsym.kind == skType and sfForward in typsym.flags:
s = typsym
addInterfaceDecl(c, s)
else:
localError(c.config, name.info, typsym.name.s & " is not a type that can be forwarded")
s = typsym
else:
s = semIdentDef(c, name, skType)
onDef(name.info, s)
s.typ = newTypeS(tyForward, c)
s.typ.sym = s # process pragmas:
if name.kind == nkPragmaExpr:
pragma(c, s, name[1], typePragmas)
if sfForward in s.flags:
# check if the symbol already exists:
let pkg = c.module.owner
if not isTopLevel(c) or pkg.isNil:
localError(c.config, name.info, "only top level types in a package can be 'package'")
else:
let typsym = pkg.tab.strTableGet(s.name)
if typsym != nil:
if sfForward notin typsym.flags or sfNoForward notin typsym.flags:
typeCompleted(typsym)
typsym.info = s.info
else:
localError(c.config, name.info, "cannot complete type '" & s.name.s & "' twice; " &
"previous type completion was here: " & c.config$typsym.info)
s = typsym
# add it here, so that recursive types are possible:
if sfGenSym notin s.flags: addInterfaceDecl(c, s)
elif s.owner == nil: s.owner = getCurrOwner(c)
if name.kind == nkPragmaExpr:
a[0][0] = newSymNode(s)
else:
a[0] = newSymNode(s)
proc checkCovariantParamsUsages(c: PContext; genericType: PType) = proc checkCovariantParamsUsages(c: PContext; genericType: PType) =
var body = genericType[^1] var body = genericType[^1]
@ -1453,8 +1461,8 @@ proc semProcAnnotation(c: PContext, prc: PNode;
var n = prc[pragmasPos] var n = prc[pragmasPos]
if n == nil or n.kind == nkEmpty: return if n == nil or n.kind == nkEmpty: return
for i in 0..<n.len: for i in 0..<n.len:
var it = n[i] let it = n[i]
var key = if it.kind in nkPragmaCallKinds and it.len >= 1: it[0] else: it let key = if it.kind in nkPragmaCallKinds and it.len >= 1: it[0] else: it
if whichPragma(it) != wInvalid: if whichPragma(it) != wInvalid:
# Not a custom pragma # Not a custom pragma

View file

@ -1567,9 +1567,44 @@ proc semTypeClass(c: PContext, n: PNode, prev: PType): PType =
result.n[3] = semConceptBody(c, n[3]) result.n[3] = semConceptBody(c, n[3])
closeScope(c) closeScope(c)
proc applyTypeSectionPragmas(c: PContext; pragmas, operand: PNode): PNode =
for p in pragmas:
let key = if p.kind in nkPragmaCallKinds and p.len >= 1: p[0] else: p
if p.kind == nkEmpty or whichPragma(p) != wInvalid:
discard "builtin pragma"
elif strTableGet(c.userPragmas, considerQuotedIdent(c, key)) != nil:
discard "User-defined pragma"
else:
# we transform ``(arg1, arg2: T) {.m, rest.}`` into ``m((arg1, arg2: T) {.rest.})`` and
# let the semantic checker deal with it:
var x = newNodeI(nkCall, key.info)
x.add(key)
if p.kind in nkPragmaCallKinds and p.len > 1:
# pass pragma arguments to the macro too:
for i in 1 ..< p.len:
x.add(p[i])
# Also pass the node the pragma has been applied to
x.add(operand.copyTreeWithoutNode(p))
# recursion assures that this works for multiple macro annotations too:
var r = semOverloadedCall(c, x, x, {skMacro, skTemplate}, {efNoUndeclared})
if r != nil:
doAssert r[0].kind == nkSym
let m = r[0].sym
case m.kind
of skMacro: return semMacroExpr(c, r, r, m, {efNoSemCheck})
of skTemplate: return semTemplateExpr(c, r, m, {efNoSemCheck})
else: doAssert(false, "cannot happen")
proc semProcTypeWithScope(c: PContext, n: PNode, proc semProcTypeWithScope(c: PContext, n: PNode,
prev: PType, kind: TSymKind): PType = prev: PType, kind: TSymKind): PType =
checkSonsLen(n, 2, c.config) checkSonsLen(n, 2, c.config)
if n[1].kind != nkEmpty and n[1].len > 0:
let macroEval = applyTypeSectionPragmas(c, n[1], n)
if macroEval != nil:
return semTypeNode(c, macroEval, prev)
openScope(c) openScope(c)
result = semProcTypeNode(c, n[0], nil, prev, kind, isType=true) result = semProcTypeNode(c, n[0], nil, prev, kind, isType=true)
# start with 'ccClosure', but of course pragmas can overwrite this: # start with 'ccClosure', but of course pragmas can overwrite this:
@ -1845,11 +1880,12 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
result.addSonSkipIntLit(child) result.addSonSkipIntLit(child)
else: else:
result = semProcTypeWithScope(c, n, prev, skIterator) result = semProcTypeWithScope(c, n, prev, skIterator)
result.flags.incl(tfIterator) if result.kind == tyProc:
if n.lastSon.kind == nkPragma and hasPragma(n.lastSon, wInline): result.flags.incl(tfIterator)
result.callConv = ccInline if n.lastSon.kind == nkPragma and hasPragma(n.lastSon, wInline):
else: result.callConv = ccInline
result.callConv = ccClosure else:
result.callConv = ccClosure
of nkProcTy: of nkProcTy:
if n.len == 0: if n.len == 0:
result = newConstraint(c, tyProc) result = newConstraint(c, tyProc)

View file

@ -5353,26 +5353,6 @@ powerful programming construct that still suffices. So the "check list" is:
(4) Else: Use a macro. (4) Else: Use a macro.
Macros as pragmas
-----------------
Whole routines (procs, iterators etc.) can also be passed to a template or
a macro via the pragma notation:
.. code-block:: nim
template m(s: untyped) = discard
proc p() {.m.} = discard
This is a simple syntactic transformation into:
.. code-block:: nim
template m(s: untyped) = discard
m:
proc p() = discard
For Loop Macro For Loop Macro
-------------- --------------
@ -6408,29 +6388,6 @@ the created global variables within a module is not defined, but all of them
will be initialized after any top-level variables in their originating module will be initialized after any top-level variables in their originating module
and before any variable in a module that imports it. and before any variable in a module that imports it.
pragma pragma
-------------
The ``pragma`` pragma can be used to declare user defined pragmas. This is
useful because Nim's templates and macros do not affect pragmas. User
defined pragmas are in a different module-wide scope than all other symbols.
They cannot be imported from a module.
Example:
.. code-block:: nim
when appType == "lib":
{.pragma: rtl, exportc, dynlib, cdecl.}
else:
{.pragma: rtl, importc, dynlib: "client.dll", cdecl.}
proc p*(a, b: int): int {.rtl.} =
result = a+b
In the example a new pragma named ``rtl`` is introduced that either imports
a symbol from a dynamic library or exports the symbol for dynamic library
generation.
Disabling certain messages Disabling certain messages
-------------------------- --------------------------
Nim generates some warnings and hints ("line too long") that may annoy the Nim generates some warnings and hints ("line too long") that may annoy the
@ -7127,6 +7084,34 @@ used. To see if a value was provided, `defined(FooBar)` can be used.
The syntax `-d:flag` is actually just a shortcut for `-d:flag=true`. The syntax `-d:flag` is actually just a shortcut for `-d:flag=true`.
User-defined pragmas
====================
pragma pragma
-------------
The ``pragma`` pragma can be used to declare user defined pragmas. This is
useful because Nim's templates and macros do not affect pragmas. User
defined pragmas are in a different module-wide scope than all other symbols.
They cannot be imported from a module.
Example:
.. code-block:: nim
when appType == "lib":
{.pragma: rtl, exportc, dynlib, cdecl.}
else:
{.pragma: rtl, importc, dynlib: "client.dll", cdecl.}
proc p*(a, b: int): int {.rtl.} =
result = a+b
In the example a new pragma named ``rtl`` is introduced that either imports
a symbol from a dynamic library or exports the symbol for dynamic library
generation.
Custom annotations Custom annotations
------------------ ------------------
It is possible to define custom typed pragmas. Custom pragmas do not effect It is possible to define custom typed pragmas. Custom pragmas do not effect
@ -7193,6 +7178,51 @@ More examples with custom pragmas:
alpha {.editRange: [0.0..1.0], animatable.}: float32 alpha {.editRange: [0.0..1.0], animatable.}: float32
Macro pragmas
-------------
All macros and templates can also be used as pragmas. They can be attached
to routines (procs, iterators, etc), type names or type expressions. The
compiler will perform the following simple syntactic transformations:
.. code-block:: nim
template command(name: string, def: untyped) = discard
proc p() {.command("print").} = discard
This is translated to:
.. code-block:: nim
command("print"):
proc p() = discard
------
.. code-block:: nim
type
AsyncEventHandler = proc (x: Event) {.async.}
This is translated to:
.. code-block:: nim
type
AsyncEventHandler = async(proc (x: Event))
------
.. code-block:: nim
type
MyObject {.schema: "schema.protobuf".} = object
This is translated to a call to the ``schema`` macro with a `nnkTypeDef`
AST node capturing both the left-hand side and right-hand side of the
definition. The macro can return a potentially modified `nnkTypeDef` tree
will replace the original row in the type section.
When multiple macro pragmas are applied to the same definition, the
compiler will apply them consequently from left to right. Each macro
will receive as input the output of the previous one.
Foreign function interface Foreign function interface
@ -7269,7 +7299,6 @@ In the example the external name of ``p`` is set to ``prefixp``. Only ``$1``
is available and a literal dollar sign must be written as ``$$``. is available and a literal dollar sign must be written as ``$$``.
Bycopy pragma Bycopy pragma
------------- -------------
@ -7313,6 +7342,7 @@ checked.
**Future directions**: GC'ed memory should be allowed in unions and the GC **Future directions**: GC'ed memory should be allowed in unions and the GC
should scan unions conservatively. should scan unions conservatively.
Packed pragma Packed pragma
------------- -------------
The ``packed`` pragma can be applied to any ``object`` type. It ensures The ``packed`` pragma can be applied to any ``object`` type. It ensures

View file

@ -212,6 +212,12 @@ proc verifyReturnType(typeName: string) {.compileTime.} =
proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
## This macro transforms a single procedure into a closure iterator. ## This macro transforms a single procedure into a closure iterator.
## The ``async`` macro supports a stmtList holding multiple async procedures. ## The ``async`` macro supports a stmtList holding multiple async procedures.
if prc.kind == nnkProcTy:
result = prc
if prc[0][0].kind == nnkEmpty:
result[0][0] = parseExpr("Future[void]")
return result
if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}: if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
error("Cannot transform this node kind into an async proc." & error("Cannot transform this node kind into an async proc." &
" proc/method definition or lambda node expected.") " proc/method definition or lambda node expected.")

View file

@ -1,6 +1,6 @@
{.experimental: "notnil".} {.experimental: "notnil".}
import macros import macros, asyncmacro, asyncfutures
block: block:
template myAttr() {.pragma.} template myAttr() {.pragma.}
@ -249,3 +249,89 @@ block:
var e {.fooBar("foo", 123, 'u').}: int var e {.fooBar("foo", 123, 'u').}: int
doAssert(hasCustomPragma(e, fooBar)) doAssert(hasCustomPragma(e, fooBar))
doAssert(getCustomPragmaVal(e, fooBar).c == 123) doAssert(getCustomPragmaVal(e, fooBar).c == 123)
block:
macro expectedAst(expectedRepr: static[string], input: untyped): untyped =
assert input.treeRepr & "\n" == expectedRepr
return input
const procTypeAst = """
ProcTy
FormalParams
Empty
IdentDefs
Ident "x"
Ident "int"
Empty
Pragma
Ident "async"
"""
type
Foo = proc (x: int) {.expectedAst(procTypeAst), async.}
static: assert Foo is proc(x: int): Future[void]
const asyncProcTypeAst = """
ProcTy
FormalParams
BracketExpr
Ident "Future"
Ident "void"
IdentDefs
Ident "s"
Ident "string"
Empty
Pragma
"""
type
Bar = proc (s: string) {.async, expectedAst(asyncProcTypeAst).}
static: assert Bar is proc(x: string): Future[void]
const typeAst = """
TypeDef
PragmaExpr
Ident "Baz"
Pragma
Empty
ObjectTy
Empty
Empty
RecList
IdentDefs
Ident "x"
Ident "string"
Empty
"""
type
Baz {.expectedAst(typeAst).} = object
x: string
static: assert Baz.x is string
const procAst = """
ProcDef
Ident "bar"
Empty
Empty
FormalParams
Ident "string"
IdentDefs
Ident "s"
Ident "string"
Empty
Empty
Empty
StmtList
ReturnStmt
Ident "s"
"""
proc bar(s: string): string {.expectedAst(procAst).} =
return s
static: assert bar("x") == "x"