I'm writing a macro "testmacro" that should take this type definition:
type Test {.testmacro.} = object
normalProc: proc(add: int): int
and generate this code:
type Test = object
normalProc: proc(add: int): int
proc normalProcGenerated(this: Test, add: int): int =
return 1
My issue is that I can't find a way to add two independent NimNodes from a single macro.
Grouping them (the TypeDef and the ProcDef) in a parent node (like a StmtList) gives an illformed AST error.
Can I achieve this kind of AST generation?
Generating he right AST can be tricky.
One way to do it is to use dumpTree on the code you want to generate:
nim> import macros
nim> dumpTree:
.... type Test = object
.... normalProc: proc(add: int): int
....
.... proc normalProcGenerated(this: Test, add: int): int =
.... return 1
....
....
StmtList
TypeSection
TypeDef
Ident "Test"
Empty
ObjectTy
Empty
Empty
RecList
IdentDefs
Ident "normalProc"
ProcTy
FormalParams
Ident "int"
IdentDefs
Ident "add"
Ident "int"
Empty
Empty
Empty
ProcDef
Ident "normalProcGenerated"
Empty
Empty
FormalParams
Ident "int"
IdentDefs
Ident "this"
Ident "Test"
Empty
IdentDefs
Ident "add"
Ident "int"
Empty
Empty
Empty
StmtList
ReturnStmt
IntLit 1
Maybe your AST is illformed not because you return a StmtList?
I tried outputting the the exact AST this code generates:
type Test = object
obj: int
proc generatedProc(this: Test, arg: int): int =
return 1
AST gen:
nnkStmtList.newTree(
nnkTypeSection.newTree(
nnkTypeDef.newTree(
newIdentNode("Test"),
newEmptyNode(),
nnkObjectTy.newTree(
newEmptyNode(),
newEmptyNode(),
nnkRecList.newTree(
nnkIdentDefs.newTree(
newIdentNode("obj"),
newIdentNode("int"),
newEmptyNode()
)
)
)
)
),
nnkProcDef.newTree(
newIdentNode("generatedProc"),
newEmptyNode(),
newEmptyNode(),
nnkFormalParams.newTree(
newIdentNode("int"),
nnkIdentDefs.newTree(
newIdentNode("this"),
newIdentNode("Test"),
newEmptyNode()
),
nnkIdentDefs.newTree(
newIdentNode("arg"),
newIdentNode("int"),
newEmptyNode()
)
),
newEmptyNode(),
newEmptyNode(),
nnkStmtList.newTree(
nnkReturnStmt.newTree(
newLit(1)
)
)
)
)
Still, illformed AST error.
The problem seems to be how to make the Nim compiler accept two new top level NimNodes in the current file with a single macro.
The issue is that from the context of the macro call via the {.testmacro.} pragma you cannot generate a procedure, because the macro is called and evaluated inside the type section. It's important to remember that Nim macros cannot look "up" the AST or in other words generate code in a place outside the calling scope of the macro.
So you get an illformed AST error, because you attempt to define a procedure inside a nnkTypeSection (by trying to generate a new stmt list with its new type section etc., but you're still inside the original type section.
The way to deal with this is usually to define a macro that takes a full type section (or just drops the type identifier and replaces it).
testmacro:
type
Test = object
normalProc: proc(add: int): int
An nnkStmtListType injects definitions into the parent scope and can be used for this.
This is a pretty dirty example, I've left out the logic for extracting the proc type, and since it involves these shadow types, naming them sensibly is important. I've got a similar example, but it's before I knew how to use nnkStmtListType, so it's really messy, I will update it.
Anyway, the basic idea is:
template foo():type =
type FooInternal = object
proc normalp(t:FooInternal,i:int):int = 5 * i
FooInternal
type Foo = foo()
let f = Foo()
echo f.normalp(5)
and with a macro it's:
import macros
macro how(x:untyped):untyped =
let typdef = nnkStmtListType.newNimNode
let res = quote do:
type Internal = object
proc normalp(f:Internal,x:int):int = x*4
Internal
res.copyChildrenTo(typdef)
result = x
result[2] = typdef
type Bar{.how.} = object
#normalp:proc(i:int):int
let b = Bar()
echo b.normalp(6)
my previous hacky example is https://github.com/shirleyquirk/nim_helpers/blob/main/dataclass.nimThe only limitation of StmtListType I've noticed is that converters defined in it do not get called.
Ideally in the future we have some kind of lazy symbol loading/cyclic dependency mechanism so type sections can be broken up into linear statements, and this kind of hackiness is not needed. In the development version var/let pragma macros can convert to any statement just fine.
Thanks! For others, I've found this feature to be partially documented here:
https://nim-lang.github.io/Nim/manual_experimental.html#extended-macro-pragmas