Hi everyone,
I was doing some experimentation with macros (so much fun! 😝), namely writing a macro for inlining functions (copy-paste their bodies basically). This is what I came up with:
import macros, macroutils
func extendedParams(f: NimNode): NimNode =
let ps = macros.params f
result = newTree(nnkFormalParams)
result.add ps[0] # return type
for i, p in ps[1..^1]:
if len(p) <= 3:
# common parameter
result.add p
else:
# multiple parameters
for name in p[0..<len(p)-2]:
result.add newTree(
nnkIdentDefs,
name,
p[^2], # type
p[^1], # default value
)
macro inline(f: proc, args: varargs[untyped]): untyped =
## Get the body of `f` and inline it using `args` as arguments.
let
impl = getImpl f
ps = extendedParams impl
bd = macros.body impl
var resultVar: NimNode
result = newTree(nnkStmtList)
if len(ps) > 0:
# Let's define a result variable in the code
if ps[0].kind != nnkEmpty:
resultVar = genSym(nskVar, "result")
result.add superQuote do:
var `resultVar`: `ps[0]`
bd.forNode(nnkSym) do (n: NimNode) -> NimNode:
if n.strVal == "result":
resultVar
else:
n
# Let's define the arguments in the code
for i, p in ps[1..^1]:
let pVar = genSym(nskLet, p[0].strVal)
result.add newTree(nnkLetSection,
newTree(nnkIdentDefs,
pVar,
p[1],
if i < len(args):
# We received a positional value
args[i]
else:
# Default value
assert p[2].kind != nnkEmpty
p[2]
)
)
bd.forNode(nnkSym) do (n: NimNode) -> NimNode:
if n.strVal == p[0].strVal:
pVar
else:
n
result.add superQuote do:
`bd`
if len(ps) > 0:
if ps[0].kind != nnkEmpty:
result.add superQuote do:
`resultVar`
result = superQuote do:
block:
`result`
# untype result
result.forNode(nnkSym) do (n: Nimnode) -> NimNode:
ident(n.strVal)
result.forNode({nnkOpenSymChoice, nnkClosedSymChoice}) do (n: Nimnode) -> NimNode:
ident(n[0].strVal)
debugEcho treeRepr result
debugEcho repr result
(This uses the amazing `macroutils` by @PMunch.)
The macro works (more or less) for simple stuff:
proc f =
echo 2 + 2
inline f
# => 4
But sometimes I get HiddenCallConv and I have a feeling this is what breaks the following code:
proc g(n: int) =
echo 2 + n
inline(g, 2)
BlockStmt
Empty
StmtList
StmtList
LetSection
IdentDefs
Ident "n"
Ident "int"
IntLit 2
Command
Ident "echo"
HiddenStdConv
Empty
Bracket
HiddenCallConv
Ident "$"
Infix
Ident "+"
IntLit 2
Ident "n"
block:
let n: int = 2
echo [2 + n]
/usercode/in.nim(83, 10) Hint: 'n' is declared but not used [XDeclaredButNotUsed]
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
How should I go about doing this?
As @xigoi said, there's no guarantee that a function will be inlined if annotated with the inline pragma. But even if that were the case, as far as I can tell, the inline pragma is ignored in the JS backend, so that's still useful.
But yes, it's a fun exercise; I'm trying to come up with some solutions for code rewriting based on getImpl, an inline macro being a fun one.
Well this still doesnt handle return but generalizing the replaceResult would help, since you could replace all returns with a break out of block that is at the root of the template. What Idid was simply using a template to do the heavy lifting for us, and using a macro to emit what we want. Yes it keeps Syms since they're already looked up and should still be functional(didn't test, but the idea is we dont want to break symbols for things we cannot import so the code works still).
import macros
proc replaceResult(body: NimNode) =
for i, x in body:
if x.kind == nnkSym and x.eqIdent("result"):
body[i] = ident"result"
else:
x.replaceResult
macro inline(p: proc, args: varargs[typed]): untyped =
let
impl = p.getImpl
templName = ident($impl[0])
result = nnkTemplateDef.newNimNode # We're emitting a nim node
impl.copyChildrenTo result
result[0] = templName # Need to rename since it's a sym
if result.params[0].kind != nnkEmpty:
result[^2].replaceResult
let
res = ident"result"
resDef = nnkVarSection.newTree newIdentDefs(res, result.params[0], newEmptyNode())
# Sandwich the code between the result declaration(we're in a template so,
# it doesnt have one normally) and a implict return.
result[^2] = newStmtList(resDef, result[^2], res)
result = newStmtList(result)
let call = newCall(templName) # make the call
for arg in args:
call.add arg # add args to call
result.add call
result = newBlockStmt(result) # Block to make it clean
echo result.treeRepr
when isMainModule:
proc f =
echo 2 + 2
proc g(n: int) =
echo 2 + n
proc h(a: int): int = a * 3 + 4
inline f
inline(g, 2)
let
a = inline(h, 20)
b = inline(h, 15)
assert a == 64
assert b == 49
Without the function definition available in a translation unit (c/cpp file) the C/C++ compiler simply can't inline the function there.
As a rule of thumb for the C and C++ backend: In the case where the calling routine is in a different module than the callee routine, if the callee routine is annotated with {.inline.} it may be inlined into the callsite, if it is not annotated with {.inline.} it will never be inlined there.
Note that this doesn't hold true when building with link-time optimizations