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.55k stars 1.47k forks source link

ProcDef is not re-evaluated between typed macros which was added to the proc pragmas by an another typed macro #18349

Open alaviss opened 3 years ago

alaviss commented 3 years ago

Example

import macros

macro foo(x: static[int], n: typed): untyped =
  result = n
  if result.kind in RoutineNodes:
    echo "foo on ", result.name, ", x: ", x
  else:
    echo "foo on ", result.kind, ", x: ", x

macro addFooToBody(x: static[int], n: typed): untyped =
  result = copyNimTree n
  result.body = newStmtList:
    newCall(bindSym"foo", newLit x):
      result.body
  echo "added foo(", x, ") to body"

macro addFoo(n: typed): untyped =
  result = copyNimTree n
  # This is also the wrong order to add the macros, see nim-lang/Nim#18348
  result.addPragma:
    nnkExprColonExpr.newTree(bindSym"addFooToBody", newLit 1)
  result.addPragma:
    nnkExprColonExpr.newTree(bindSym"foo", newLit 2)

proc test() {.used, addFoo.} =
  discard

Current Output

added foo(1) to body
foo on test, x: 2
foo on nnkDiscardStmt, x: 1

Expected Output

added foo(1) to body
foo on nnkDiscardStmt, x: 1
foo on test, x: 2

Additional Information

$ nim -v
Nim Compiler Version 1.5.1 [Linux: amd64]
Compiled at 2021-06-24
Copyright (c) 2006-2021 by Andreas Rumpf

git hash: 0f91b67f5c15328330f74a8769aed9961940aab2
active boot switches: -d:release -d:nimUseLinenoise
metagn commented 3 weeks ago

Somewhat simplified:

import macros

macro c(n: typed): untyped =
  result = n
  echo "c"
  echo result.repr

macro b(n: typed): untyped =
  result = copyNimTree n
  result.body = newStmtList(ident"abc") # this is supposed to error
  echo "b"
  echo result.repr

macro a(n: typed): untyped =
  result = copyNimTree n
  result.addPragma(bindSym"b")
  result.addPragma(bindSym"c")
  echo "a"
  echo result.repr

proc test() {.a.} =
  discard
a
proc test() {.b, c.} =
  discard

b
proc test() {.c.} =
  abc

c
proc test() =
  abc

a.nim(24, 15) template/generic instantiation of `a` from here
a.nim(18, 12) template/generic instantiation of `b` from here
a.nim(20, 12) template/generic instantiation of `c` from here
a.nim(10, 34) Error: undeclared identifier: 'abc'

My guess is when c is getting called, the proc test() = abc argument is considered typed since it has type tyVoid, and so not rechecked by prepareOperand. Using calls instead of pragmas works, because the next macro receives a different node than the proc node, a call node, always having a nil type.

We can fix this case (checking for formal.kind == tyTyped and a.typ.kind == tyVoid in prepareOperand as well as a.typ == nil), but I don't know about the general case, when the typed argument has non-void type. We could automatically set the type to nil for every macro output with type untyped, but this could break stuff, and there's the question of it being applied recursively. Another option is maybe exposing something like eraseType(node) which sets the type of the node to nil. but maybe this makes users depend on an implementation detail.

There's also the question of "should prepareOperand check for nil type at all", when it could just sem everything given to it. I don't know if this is feasible but as seen in #16867 other things like nkHiddenCallConv have similar problems.

This is effectively the same issue as #18348.