Philipp Doerner
Philipp Doerner

Reputation: 1892

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.

Upvotes: 3

Views: 699

Answers (1)

Philipp Doerner
Philipp Doerner

Reputation: 1892

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.

Upvotes: 3

Related Questions