/
06.05.2025 at 05:18 pm
Cuttings

Nim / Exercise - `timed(expr)` macro

Measure and print how long an expression takes to run.
timed(sleep(1000))
# Output: took 1001 ms

Hint:

  • Use times module and cpuTime or epochTime.

  • Inject start, end, duration calculation, then print.

Table of Contents

With Approach 1

  1. 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:.

  2. 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.

  3. 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.

  4. 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.


With Approach 2

  1. genSym stands for generate symbol. It creates a uniquely named identifier, like time_taken_123456.

  2. 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.

Code (SolutiON)
     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))

                    
Source: Nim Macros - Beginner & Intermediate Exercises
Filed under:
#
#
Words: 408 words approx.
Time to read: 1.63 mins (at 250 wpm)
Keywords:
, , , , , , , , ,

Latest Comments

© Wan Zafran. See disclaimer.