I have been writing some helper macros to assist in the coding of destructors, and I ran into the following problem.
import macros
macro dumpCompiledCode(compiledCode: typed): untyped =
## Simple macro to dump the final generated source code form of the argument,
## after all nested macros have been called, template code has been inserted, etc.
echo "\n#### final generated code:"
echo repr(compiledCode)
# Return what was passed in so that compilation can continue
result = compiledCode
macro refObjectDestructor(typeName: typed, bodyCode: untyped): untyped =
## Macro to construct a destructor proc definition for a ref object type
## The generated proc definition looks something like:
## proc `=destroy`(x: typeof(<typeName>()[])) =
## <bodyCode>
assert typeName.kind == nnkSym
assert typeName.symKind == nskType
assert bodyCode.kind == nnkStmtList
# destructor proc name definition ("`=destroy`")
var procNameNode = newNimNode(nnkAccQuoted)
procNameNode.add(newIdentNode("="))
procNameNode.add(newIdentNode("destroy"))
# destructor proc return type & parameters
var paramNodes = newSeq[NimNode]()
# Return type - none
let returnTypeNode = newEmptyNode()
paramNodes.add(returnTypeNode)
# First (and only) parameter definition
var param1Node = newNimNode(nnkIdentDefs)
param1Node.add(newIdentNode("x")) # parameter name
# Need to build the AST for the parameter type ("typeof <typeName>()[]")
# Make an instance of the ref object: "<typeName>()"
let refInstantiationNode = newCall(newIdentNode(typeName.strval))
# Dereference the ref instance to get the object: "<typeName>()[]"
var derefedObjectNode = newNimnode(nnkBracketExpr)
derefedObjectNode.add(refInstantiationNode)
# Get the (anonymous) type of the object: "typeof(<typeName>()[])"
let objectTypeNode = newCall("typeof", derefedObjectNode)
# Now add the type to the parameter definition
param1Node.add(objectTypeNode) # parameter type
param1Node.add(newEmptyNode())
# Add the parameter definition to the proc parameters node
paramNodes.add(param1Node)
# Finally, generate the proc definition
result = newProc(procNameNode, paramNodes, bodyCode)
when isMainModule:
type
TestRef = ref object of RootRef
name: string
Marker = ref object
tref: TestRef
proc `=destroy`(x: typeof(TestRef()[])) =
`=destroy`(x.name)
# The compiler crashes after the final generated code has been printed
# If *either* of the noted changes is made, then compilation succeeds
dumpCompiledCode: refObjectDestructor(Marker): # (1) Compiles okay if "dumpCompiledCode: " is removed
`=destroy`(x.tref) # (2) Compiles okay if a defererence is added to the argument, i.e. "x.tref[]"
static: echo "##### Done"
When I compile the above code the compiler crashes - after the dumpCompiledCode macro has printed the generated code (which looks okay). The output is:
Hint: used config file '/opt/nim-2.0.4/config/nim.cfg' [Conf]
Hint: used config file '/opt/nim-2.0.4/config/config.nims' [Conf]
........................................................................
#### final generated code:
proc `=destroy`(x: typeof(Marker()[])) {.raises: [].} =
`=destroy`(x.tref)
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Segmentation fault (core dumped)
Applying code change (1) above causes the compilation to succeed, but then of course I don't get to see the generated code.
Applying code change (2) also causes the compilation to succeed, and the generated code is printed. But I strongly suspect that the object's reference count would not be decremented by the destructor call - negating the whole point of the call.
If the the field x.tref is replaced by a non-ref entity (object, string, seq, even a seq of ref) this issue does not occur.
So what's going on? And how do I get past this?
AST generated by the refObjectDestructor macro (obtained by inserting echo treerepr(result) at the end of the macro):
ProcDef
AccQuoted
Ident "="
Ident "destroy"
Empty
Empty
FormalParams
Empty
IdentDefs
Ident "x"
Call
Ident "typeof"
BracketExpr
Call
Ident "Marker"
Empty
Empty
Empty
StmtList
Call
AccQuoted
Ident "="
Ident "destroy"
DotExpr
Ident "x"
Ident "tref"
I then dumped the AST of the code that the macro was intended to generate:
dumpTree:
proc `=destroy`(x: typeof(Marker()[])) =
`=destroy`(x.tref)
The resulting AST is:
StmtList
ProcDef
AccQuoted
Ident "="
Ident "destroy"
Empty
Empty
FormalParams
Empty
IdentDefs
Ident "x"
Call
Ident "typeof"
BracketExpr
Call
Ident "Marker"
Empty
Empty
Empty
StmtList
Call
AccQuoted
Ident "="
Ident "destroy"
DotExpr
Ident "x"
Ident "tref"
The only difference between the two is an enclosing StmtList node in the latter. I tried altering the refObjectDestructor macro to include the enclosing StmtList. It has no effect; the compiler still crashes.
An additional piece of information. I passed the equivalent source code to the dumpCompiledCode macro instead of the output from refObjectDestructor:
dumpCompiledCode:
proc `=destroy`(x: typeof(Marker()[])) =
`=destroy`(x.tref)
The result was identical. The original code was printed, with the addition of a raises pragma, and then the compiler crashed.
proc `=destroy`(x: typeof(Marker()[])) {.raises: [].} =
`=destroy`(x.tref)
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Segmentation fault (core dumped)
So the problem seems to be with the dumpCompiledCode macro.
macro dumpCompiledCode(compiledCode: typed): untyped =
## Simple macro to dump the final generated source code form of the argument
## after all nested macros have been called, template code has been inserted, etc.
echo "\n#### final generated code:"
echo repr(compiledCode)
# Return what was passed in so that compilation can continue
result = compiledCode
However, as I mentioned in the OP, the problem only occurs for a destructor call with a ref object argument. It works fine if the argument is a non-ref.