compile-timenim-langcompile-time-constantloop-unrolling

How to iterate over a compile-time seq in a manner that unrolls the loop?


I have a sequence of values that I know at compile-time, for example: const x: seq[string] = @["s1", "s2", "s3"]

I want to loop over that seq in a manner that keeps the variable a static string instead of a string as I intend to use these strings with macros later.

I can iterate on objects in such a manner using the fieldPairs iterator, but how can I do the same with just a seq?

A normal loop such as

for s in x:
  echo s is static string

does not work, as s will be a string, which is not what I need.


Solution

  • The folks over at the nim forum were very helpful (here the thread).

    The solution appears to be writing your own macro to do this. 2 solutions I managed to make work for me were from the users mratsim and a specialized version from hlaaftana

    Hlaaftana's version:

    This one unrolls the loop over the various values in the sequence. By that I mean, that the "iterating variable s" changes its value and is always the value of one of the entries of that compile-time seq x (or in this example a). In that way it functions basically like a normal for-in loop.

    import macros
    
    macro unrollSeq(x: static seq[string], name, body: untyped) =
      result = newStmtList()
      for a in x:
        result.add(newBlockStmt(newStmtList(
          newConstStmt(name, newLit(a)),
          copy body
        )))
    
    const a = @["la", "le", "li", "lo", "lu"]
    unrollSeq(a, s):
      echo s is static
      echo s
    

    mratsim's version:

    This one doesn't unroll a loop over the values, but over a range of indices. You basically tell the staticFor macro over what range of values you want an unrolled for loop and it generates that for you. You can access the individual entries in the seq then with that index.

    import std/macros
    
    proc replaceNodes(ast: NimNode, what: NimNode, by: NimNode): NimNode =
      # Replace "what" ident node by "by"
      proc inspect(node: NimNode): NimNode =
        case node.kind:
        of {nnkIdent, nnkSym}:
          if node.eqIdent(what):
            return by
          return node
        of nnkEmpty:
          return node
        of nnkLiterals:
          return node
        else:
          var rTree = node.kind.newTree()
          for child in node:
            rTree.add inspect(child)
          return rTree
      result = inspect(ast)
    
    macro staticFor*(idx: untyped{nkIdent}, start, stopEx: static int, body: untyped): untyped =
      result = newStmtList()
      for i in start .. stopEx: # Slight modification here to make indexing behave more in line with the rest of nim-lang
        result.add nnkBlockStmt.newTree(
          ident("unrolledIter_" & $idx & $i),
          body.replaceNodes(idx, newLit i)
        )
    
    staticFor(index, x.low, x.high):
      echo index
      echo x[index] is static string
    

    Elegantbeefs version

    Similar to Hlaaftana's version this unrolls the loop itself and provides you a value, not an index.

    import std/[macros, typetraits]
    
    proc replaceAll(body, name, wth: NimNode) =
      for i, x in body:
        if x.kind == nnkIdent and name.eqIdent x:
          body[i] = wth
        else:
          x.replaceAll(name, wth)
    
    template unrolledFor*(nameP, toUnroll, bodyP: untyped): untyped =
      mixin
        getType,
        newTree,
        NimNodeKind,
        `[]`,
        add,
        newIdentDefs,
        newEmptyNode,
        newStmtList,
        newLit,
        replaceAll,
        copyNimTree
      
      macro myInnerMacro(name, body: untyped) {.gensym.} =
        let typ = getType(typeof(toUnroll))
        result = nnkBlockStmt.newTree(newEmptyNode(), newStmtList())
        result[^1].add nnkVarSection.newTree(newIdentDefs(name, typ[^1]))
        for x in toUnroll:
          let myBody = body.copyNimTree()
          myBody.replaceAll(name, newLit(x))
          result[^1].add myBody
    
      myInnerMacro(nameP, bodyP)
    
    const x = @["la", "le", "Li"]
    
    unrolledFor(value, x):
      echo value is static
      echo value
    

    All of them are valid approaches.