timed(sleep(1000)) # Output: took 1001 ms
Hint:
Use
times
module andcpuTime
orepochTime
.Inject start, end, duration calculation, then print.
To avoid confusion, be mindful how identifiers are resolved inside a macro-generated AST - i.e. macro scoping:
A macro's result must be one cohesive block (or expression) that can be inserted at the call site. This applies to the entire quote do:
block, which becomes a single node in the AST.
epochTime()
and cpuTime()
are runtime functions, so they should go inside quote do:
.
Why fmt
might not work inside macros:
It's not a runtime error - it's a macro AST issue: the name time_taken
might not be bound in the AST evaluation scope (thus why you might encounter 'undeclared identifier' errors).
fmt
wants to run on a valid identifier at runtime. Thus be careful when the macro is being expanded.
Only use backticks to inject variable names, never to access them inside string interpolation.
fmt
interpolates at macro expansion time, not runtime. String interpolation is more demanding about scopes than a normal function call like echo
. Say when writing:
echo fmt"took {timeTaken} ms"
Nim parses the entire fmt"..."
string at compile time, turning the {timeTaken}
part into an expression.
The issue: in a macro, you're working with code templates (ASTs). When you write things like:
let `timeTaken` = ...
This tells the macro: "inject a new variable here whose name is stored in the symbol timeTaken
." That works fine for declaring or assigning.
But when you do this:
echo fmt"took {`timeTaken`} ms"
...Nim tries to parse it as if it were a literal identifier:
`{`timeTaken`}`
The above is not valid syntax. Instead, fmt
sees:
{`timeTaken`}
...which is not a valid Nim identifier.
Analogy for the above:
Imagine fmt"..."
is like a fill-in-the-blanks postcard:
"Hello, {name}! You are {age} years old."
The compiler needs to see the actual variable names (name
, age
) to know what to plug in. But if you try:
"Hello, {`name`}! You are {`age`} years old."`
...the postcard printer has no idea what `name`
means.
genSym
stands for generate symbol. It creates a uniquely named identifier, like time_taken_123456
.
There's no benefit in the genSym
approach here. The code is short, and the macros are not generating code that might inject identifiers that clash with existing identifiers around the call site.
import macros
import times
import std/os
import std/strformat
macro timed_1(expr: untyped): untyped =
result = quote:
let time_start = epochTime()
`expr`
let time_taken = (epochTime() - time_start) * 1000
# echo fmt"took {time_taken:.0f} ms" # FAILS
echo "took ", time_taken, " ms"
macro timed_2_gensym(expr: untyped): untyped =
let timeStart = genSym(nskLet, "time_start")
let timeTaken = genSym(nskLet, "time_taken")
let msg = genSym(nskLet, "msg")
result = quote do:
let `timeStart` = epochTime()
`expr`
let `timeTaken` = (epochTime() - `timeStart`) * 1000
# echo fmt"took {`timeTaken`} ms" # FAILS
# let `msg` = fmt"took {`timeTaken`:.0f} ms" # FAILS
echo "took ", `timeTaken`, " ms" # WORKS
timed_1(sleep(1000))
timed_2_gensym(sleep(1000))