nim-lang / Nim

Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula. Its design focuses on efficiency, expressiveness, and elegance (in that order of priority).
https://nim-lang.org
Other
16.54k stars 1.47k forks source link

Rules for custom pragma push/pop #12867

Open mratsim opened 4 years ago

mratsim commented 4 years ago

The manual does an artistic dodge about the push/pop rules for custom pragma:

https://nim-lang.org/docs/manual.html#pragmas-push-and-pop-pragmas

For third party pragmas it depends on its implementation, but uses the same syntax.

I have found one way where it works:

template m() {.pragma.}

{.push m.}
proc p() = discard
{.pop.}

[Bad example see comments below] ~but change it to:~

template m(s: string) {.pragma.}

{.push m.}
proc p() = discard
{.pop.}

~and you get invalid pragma~

Change it to

template m(s: untyped) = discard

{.push m.}
proc p() = discard
{.pop.}

from https://nim-lang.org/docs/manual.html#macros-macros-as-pragmas

And it still doesn't work. And it doesn't work with macro either.

Alternative approach

Let's thing out-of-the-box and maybe it's because templates are not well registered as pragma, does a well known pragma work?

{.push inline.}
proc foo(x: int) =
  echo "Hello there"
{.pop.}

Yes it does.

Does aliasing it work

{.pragma: myPragma, inline.}
proc foo(x: int) {.myPragma.}=
  echo "Hello there"

Yes it does

Does pushing my aliased pragma work?

{.pragma: myPragma, inline.}

{.push myPragma.}
proc foo(x: int) =
  echo "Hello there"
{.pop.}

No it doesn't. the error is now Error: recursive dependency: myPragma

Examples from within Nim

Looking for custom push/pop yields only 2 tests from nimpretty with a pretty sketchy syntax so I supposed it's not supposed to work: https://github.com/nim-lang/Nim/blob/5929c3da218fdecf70e11ff970e7b7cda50c9144/nimpretty/tests/exhaustive.nim#L51-L60

Trace

Traceback (most recent call last)
.../Nim/compiler/nim.nim(106) nim
.../Nim/compiler/nim.nim(83) handleCmdLine
.../Nim/compiler/cmdlinehelper.nim(98) loadConfigsAndRunMainCommand
.../Nim/compiler/main.nim(188) mainCommand
.../Nim/compiler/main.nim(92) commandCompileToC
.../Nim/compiler/modules.nim(144) compileProject
.../Nim/compiler/modules.nim(85) compileModule
.../Nim/compiler/passes.nim(216) processModule
.../Nim/compiler/passes.nim(86) processTopLevelStmt
.../Nim/compiler/sem.nim(600) myProcess
.../Nim/compiler/sem.nim(568) semStmtAndGenerateGenerics
.../Nim/compiler/semstmts.nim(2214) semStmt
.../Nim/compiler/semexprs.nim(986) semExprNoType
.../Nim/compiler/semexprs.nim(2758) semExpr
.../Nim/compiler/semstmts.nim(1990) semProc
.../Nim/compiler/semstmts.nim(1841) semProcAux
.../Nim/compiler/pragmas.nim(1175) implicitPragmas
.../Nim/compiler/pragmas.nim(1147) singlePragma
.../Nim/compiler/pragmas.nim(727) semCustomPragma
.../Nim/compiler/pragmas.nim(103) invalidPragma
.../Nim/compiler/msgs.nim(549) localError
.../Nim/compiler/msgs.nim(531) liMessage
.../Nim/compiler/msgs.nim(361) handleError
.../Nim/compiler/msgs.nim(346) quit
FAILURE

https://github.com/nim-lang/Nim/blob/5929c3da218fdecf70e11ff970e7b7cda50c9144/compiler/pragmas.nim#L710-L728

mratsim commented 4 years ago

I may have misunderstood but I thing it works with no argument but doesn't as soon as we pass an argument due to semOverloadedCall trying to resolve overloading. It manage to with no arguments but for pragma the templates are not instantiated or maybe it doesn't work that well with the procAST as argument:

https://github.com/nim-lang/Nim/blob/5929c3da218fdecf70e11ff970e7b7cda50c9144/compiler/semcall.nim#L529-L568

nc-x commented 4 years ago

I am not much familiar with custom pragmas, but if m takes an argument, then don't you need to pass an argument to m in push for it to work?

for e.g., this compiles -

template m(s: string) {.pragma.}

{.push m("s").}
proc p() = discard
{.pop.}

(I am on mobile, so sorry if i missed something)

mratsim commented 4 years ago

Yes you're right, my second example doesn't make sense and your fix actually compiles.

Template as pragma or macro as pragma which manipulates the proc body does the following transformation

template myPragma(a, b, c: int, procBody:untyped): untyped =
  discard

proc foo(){.myPragma(1,2,3).} =
  discard

is rewritten into

template myPragma(a, b, c: int, procBody:untyped): untyped =
  discard

myPragma(1, 2, 3):
  proc foo() =
    discard

This allows a macro to manipulate the proc AST, it just needs to have "untyped" as the last parameter type. The last 2 compiles. But if I want to apply this pragma to a whole file with push/pop the compiler refuses unless I missed some hidden rules.

This would be very useful to write inspection libraries for:

where you could just add hooks at the beginning of a proc and at all the exit points in an unintrusive and maintainable way, instead of tagging everything manually and remember to tag the new procs when they are introduced, lest they don't show up in your system.

nc-x commented 4 years ago

If you manually apply the custom pragma to a proc, the proc is added as an argument to the pragma automatically. so the template/macro needs to have an untyped as the last argument. If it has zero arguments, then compiler says that the pragma is not found.

For push, we do not currently add the proc as the last argument. So instead the reverse case works. macros/templates without any extra untyped args work. macros/templates with extra untyped args do not work.

So the solution is to add the proc as the last argument here as well and document (if not already) / give better error message that custom pragmas need to have last parameter as untyped.

Solution: in semCustomPragma, to the nkCall we add the last node as the proc (without the pragma) itself for manually applied pragma, it is already done here - https://github.com/nim-lang/Nim/blob/56cf3403b48943fe1fbc26377669d7d69fde4878/compiler/semstmts.nim#L1426-L1430

alternative fix could be to change to order so as to apply push pragma before sem runs on proc, so that in the end both the above cases follow the same code path (which is not the case now).) but that would be a more difficult approach, and maybe not even possible

D-Nice commented 4 years ago
{.pragma: myPragma, inline.}

{.push myPragma.}
proc foo(x: int) =
  echo "Hello there"
{.pop.}
No it doesn't. the error is now Error: recursive dependency: myPragma

I ran into this same issue, expecting push to work.

arkanoid87 commented 3 years ago

faced same problem while trying to {.push cov.} from https://github.com/yglukhov/coverage

n0bra1n3r commented 3 years ago

Currently I use this hack

macro scan*(tag: int) =
  let file = staticRead(tag.lineinfoObj.filename)
  for node in parseStmt(file):
    ...

as a workaround to scan any module its called in to add code based on the module's contents. Would be great to be able to implement scan properly if this issue gets fixed.

jlokier commented 3 years ago

Like others, I expected {.push.} to work with macro pragmas, and encountered the same problem. Also like others, I read the manual and it seems to imply pushing custom macros is possible, so I wasted a fair amount of time trying to figure out what I was missing before finding this issue and realising the manual is kind of misleading.

One of my uses is logging all parameters and return values with a proc-transforming macro. Another is making a wrapper for each function to map its parameter types for a C interface, that it also callable from Nim using Nim native types. A third is converting procs to pairs of procs that are both run and compared to do differential testing.

All these uses need to be switched on or off for different builds, so it would have been very convenient to use push under a when guard at the start of the file. The current situation requires something like {.current_build_options.} after every proc, where current_build_options is a macro pragma which applies the real selection of pragmas.

markspanbroek commented 3 years ago

I would also love to see this fixed. I'm currently using a clunky workaround with a custom push macro.

akbcode commented 3 years ago

Would like to see this fixed/implemented as well. Useful for all the reasons mentioned above - logging, benchmarks, code contracts, instrumentation, code generation, DSL, etc.

treeform commented 2 years ago

I just run into this bug as well, was going to create a new bug, so I have a simple repro case:

import macros

macro mz*(fn: untyped) =
  return fn

proc foo1() {.mz.} = discard
proc bar2() {.mz.} = discard

{.push mz.}
proc foo2() = discard
proc bar2() = discard
{.pop.}

The push/pop should work, it just does not.

custompragma.nim(9, 8) Error: invalid pragma: mz
Nim Compiler Version 1.6.0 [MacOSX: amd64]
metagn commented 2 years ago

The thing is if this was implemented you would be passing a random macro to push and the compiler would only be assuming that it's for routines, which is kind of unintuitive. But I disagree with the idea of annotating the original macro so it can be used as a pragma, so I don't know a good solution. For now a weak version of this can be:

import macros

macro mz*(fn: untyped) =
  return fn

proc foo1() {.mz.} = discard
proc bar2() {.mz.} = discard

macro applyPragma(prag, st) =
  if st.kind == nnkStmtList:
    for s in st:
      s.addPragma(prag)
  else:
    s.addPragma(prag)

applyPragma mz:
  proc foo2() = discard
  proc bar2() = discard

You can also make this work recursively if you wanted, but you would have to use typed for more complex code which wouldn't work for some macros that require untyped like async.

mratsim commented 2 years ago

We can annotate the original macro with smething like

macro mz*(fn: untyped{nkProcDef, nkFuncDef}) =
  return fn

using the syntax for parameter constraints https://nim-lang.github.io/Nim/manual_experimental.html#term-rewriting-macros-parameter-constraints

Menduist commented 2 years ago

To add to this issue,

template hey {.pragma.}
{.push hey.}
proc hello = discard # works
iterator noo: int = discard # fails
/tmp/test.nim(4, 1) template/generic instantiation from here
/tmp/test.nim(2, 8) Error: cannot attach a custom pragma to 'noo'

This is a pain point when using for instance async, since the inner iterator will get the custom pragma inferred, and will then fail to compile