Skip to content

Instantly share code, notes, and snippets.

@demotomohiro
Created November 20, 2019 05:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save demotomohiro/8c7c11516df46f009339704bfb713cf6 to your computer and use it in GitHub Desktop.
Save demotomohiro/8c7c11516df46f009339704bfb713cf6 to your computer and use it in GitHub Desktop.
Macro that automatically generate object type definition from constructor.
import macros, strutils
# Some string that no one use for variable name.
# It was generated with following command:
# openssl rand -base64 16
const fieldMarker = "Ew55GZ4wPFop9pbAYp9RQ"
macro fields*(stmts: untyped): untyped =
stmts.expectKind nnkStmtList
result = newTree(
nnkVarSection,
newIdentDefs(ident(fieldMarker), ident"int"))
for i in stmts:
if i.kind == nnkAsgn:
result.add newIdentDefs(i[0], newEmptyNode(), i[1])
elif i.kind == nnkVarSection:
i.copyChildrenTo result
elif i.kind == nnkCommentStmt:
continue
else:
error "You can use only assignment and var section to `field` macro.", stmts
#Add used pragma to all ident.
for i, son in result.pairs:
son.expectKind nnkIdentDefs
for j in 0..(son.len - 3):
let identNode = newTree(
nnkPragmaExpr,
son[j],
newTree(nnkPragma, ident"used"))
result[i][j] = identNode
macro objectDef*(procDef: typed): untyped =
proc isFieldSect(n: NimNode): bool =
n.kind == nnkVarSection and
n.len != 0 and
n[0].kind == nnkIdentDefs and
n[0][0].strVal == fieldMarker
proc readField(recList, src: NimNode) =
if isFieldSect(src):
for i in src:
i.expectKind nnkIdentDefs
if i[0].strVal == fieldMarker:
continue
if i[^1].kind == nnkEmpty:
recList.add i.copy
else:
let identDefs = i.copy
identDefs[^2] = i[^1].getTypeInst
identDefs[^1] = newEmptyNode()
recList.add identDefs
proc ToAsgnResult(field: NimNode): NimNode =
proc toIdentsRecur(src: NimNode) =
for i, n in src:
if n.kind == nnkSym and n.getImpl.kind == nnkNilLit:
src[i] = ident(n.strVal)
else:
toIdentsRecur(n)
var newBlock = newStmtList()
for j in field:
j.expectKind nnkIdentDefs
if j[^1].kind == nnkEmpty:
continue
else:
let left = newDotExpr(
ident"result",
ident(j[0].strVal))
#Convert symbols that refer to local variable or argument to ident.
toIdentsRecur(j[2])
newBlock.add newAssignment(left, j[2])
newBlockStmt(newBlock)
procDef.expectKind nnkProcDef
let name = procDef.name.strVal
assert name.startsWith("the")
var objName = name
objName.removePrefix("the")
let objNameIdent = ident(objName)
var recList = newNimNode(nnkRecList, procDef)
if procDef.body.kind == nnkVarSection:
recList.readField(procDef.body)
else:
for i in procDef.body:
recList.readField(i)
if recList.len == 0:
warning "There is no field", procDef
for i in recList:
i[0] = ident(i[0].strVal)
#echo "recList.repr:"
#echo recList.treeRepr
let typDef = quote do:
type `objNameIdent` = object
typDef[0][2][2] = recList
#echo "typDef:"
#echo typDef.treeRepr
let newProcDef = procDef.copy
newProcDef.name = ident("init" & objName)
newProcDef.params[0] = objNameIdent
if isFieldSect(newProcDef.body):
newProcDef.body = newProcDef.body.ToAsgnResult()
else:
for i, n in newProcDef.body.pairs:
if isFieldSect(n):
newProcDef.body[i] = n.ToAsgnResult()
#echo "newProcDef.treeRepr"
#echo newProcDef.treeRepr
newStmtList(typDef, newProcDef)
when isMainModule:
import sets
when false:
expandMacros:
fields:
x = 1 + 1
var
a, b, c: int
s, t = "aa"
expandMacros:
proc theFoo() {.objectDef, used.} =
fields:
## field
x = 1 + 2
y = "foo " & "bar"
# const n = 1.1
# result.z = test(n)
expandMacros:
proc theBar(arg: int) {.objectDef, used.} =
var a = 1
let ary = ['a', 'b', 'c']
fields:
b = a + 2
c = initHashSet[int](arg)
d = toHashSet(ary)
var x = 1.0
var
i,j,k: array[3, uint]
y: int
let a = initFoo()
echo a.repr
let b = initBar(4)
doAssert b.b == 3
doAssert b.c is HashSet[int]
doAssert b.c.len == 0
doAssert b.d == toHashSet(['a', 'b', 'c'])
doAssert b.x == 1.0
doAssert b.i is array[3, uint]
echo b.repr
@demotomohiro
Copy link
Author

What I want to do is when I write following code:

proc initFoo(arg: int) {.objectDef.} =
  fields:
    x = arg
    y = initHashSet[int](arg)

objectDef macro generate following code:

type
  Foo = object
    x: int
    y: HashSet[int]

proc initFoo(arg: int): Foo =
  result.x = arg
  result.y = initHashSet[int](arg)

@Vindaar
Copy link

Vindaar commented Nov 20, 2019

The only thing I can come up with is the following. Replaces the magic fieldMarker by a custom pragma annotation. Doesn't simplify the whole thing all that much of course. But to be honest I find the code pretty elegant already given what it's doing.

import macros, strutils, sets, tables

# to know whether it's one of our regions
template customFieldRegion(): untyped {.pragma.}

macro fields*(stmts: untyped): untyped =
  stmts.expectKind nnkStmtList
  var vars = nnkVarSection.newTree()
  for i in stmts:
    if i.kind == nnkAsgn:
      vars.add newIdentDefs(i[0], newEmptyNode(), i[1])
    elif i.kind == nnkVarSection:
      i.copyChildrenTo vars
    elif i.kind == nnkCommentStmt:
      continue
    else:
      error "You can use only assignment and var section to `field` macro.", stmts

  #Add used pragma to all ident.
  for i, son in vars.pairs:
    son.expectKind nnkIdentDefs
    for j in 0..(son.len - 3):
      let identNode = newTree(
                              nnkPragmaExpr,
                              son[j],
                              newTree(nnkPragma, ident"used"))
      vars[i][j] = identNode
  let res = newStmtList(vars)
  result = newStmtList(nnkPragmaBlock.newTree(
    nnkPragma.newTree(ident"customFieldRegion"), res)
  )

macro objectDef*(procDef: typed): untyped =
  proc isFieldSect(n: NimNode): bool =
    result = n.kind == nnkPragmaBlock and n[0][0].strVal == "customFieldRegion"

  proc readField(recList, node: NimNode) =
    if isFieldSect(node):
      # get statements in pragma block
      let src = node[1]
      for i in src:
        i.expectKind nnkIdentDefs
        if i[^1].kind == nnkEmpty:
          recList.add i.copy
        else:
          let identDefs = i.copy
          identDefs[^2] = i[^1].getTypeInst
          identDefs[^1] = newEmptyNode()
          recList.add identDefs

  proc ToAsgnResult(field: NimNode): NimNode =
    proc toIdentsRecur(src: NimNode) =
      for i, n in src:
        if n.kind == nnkSym and n.getImpl.kind == nnkNilLit:
          src[i] = ident(n.strVal)
        else:
          toIdentsRecur(n)

    var newBlock = newStmtList()
    for j in field:
      j.expectKind nnkIdentDefs
      if j[^1].kind == nnkEmpty:
        continue
      else:
        let left = newDotExpr(
                              ident"result",
                              ident(j[0].strVal))
        #Convert symbols that refer to local variable or argument to ident.
        toIdentsRecur(j[2])
        newBlock.add newAssignment(left, j[2])
    newBlockStmt(newBlock)

  procDef.expectKind nnkProcDef
  let name = procDef.name.strVal
  assert name.startsWith("the")
  var objName = name
  objName.removePrefix("the")

  let objNameIdent = ident(objName)

  var recList = newNimNode(nnkRecList, procDef)
  # check if pragma block
  if procDef.body.kind == nnkPragmaBlock:
    recList.readField(procDef.body)
  else:
    for i in procDef.body:
      recList.readField(i)

  if recList.len == 0:
    warning "There is no field", procDef

  for i in recList:
    i[0] = ident(i[0].strVal)

  let typDef = quote do:
    type `objNameIdent` = object
  typDef[0][2][2] = recList

  let newProcDef = procDef.copy
  newProcDef.name = ident("init" & objName)
  newProcDef.params[0] = objNameIdent
  if isFieldSect(newProcDef.body):
    # extract statements of pragma block
    let realBody = newProcDef.body[1]
    newProcDef.body = realBody.ToAsgnResult()
  else:
    for i, n in newProcDef.body.pairs:
      if isFieldSect(n):
        # work on content of pragma block
        newProcDef.body[i] = n[1].ToAsgnResult()

  result = newStmtList(typDef, newProcDef)

when isMainModule:
  import sets

  expandMacros:
    proc theFoo() {.objectDef, used.} =
      fields:
        ## field
        x = 1 + 2
        y = "foo " & "bar"
      # const n = 1.1
      # result.z = test(n)

  expandMacros:
    proc theBar(arg: int) {.objectDef, used.} =
      var a = 1
      let ary = ['a', 'b', 'c']
      fields:
        b = a + 2
        c = initHashSet[int](arg)
        d = toHashSet(ary)
        var x = 1.0
        var
          i,j,k: array[3, uint]
          y: int

  let a = initFoo()

  echo a.repr

  let b = initBar(4)
  doAssert b.b == 3
  doAssert b.c is HashSet[int]
  doAssert b.c.len == 0
  doAssert b.d == toHashSet(['a', 'b', 'c'])
  doAssert b.x == 1.0
  doAssert b.i is array[3, uint]
  echo b.repr

@demotomohiro
Copy link
Author

Vindaar, Thank you!
It is simpler than using random variable name.

@demotomohiro
Copy link
Author

I thought this code could be simpler if I can use getTypeInst from untyped parameters or call a macro with typed parameters from macro with untyped parameters.
But it seems these are impossible.
nim-lang/Nim#7739
nim-lang/RFCs#44

@Vindaar
Copy link

Vindaar commented Nov 21, 2019

いいえ!
\tiny hope this is correct

if I can use getTypeInst from untyped parameters

This works, but only under specific circumstances.

  • determining the type of an arbitrary untyped expression is not possible from an untyped marco, as far as I can tell
  • for procs, global vars etc. we can use bindSym and apply getType* on that symbol to access the type information that way, e.g.
import macros

proc testA(x: int): float =
  discard

macro stuff(callStuff: untyped): untyped =
  let procSym = bindSym("testA")
  echo procSym.getTypeInst.treeRepr

stuff(5)

which prints

ProcTy
  FormalParams
    Sym "float"
    IdentDefs
      Sym "x"
      Sym "int"
      Empty
  Empty

But yes, in your example the problem as far as I see it would be to deduce the types of expressions like x = 1 + 1, which cannot be done that way unfortunately (maybe there's some hacky workaround, not sure).

or call a macro with typed parameters from macro with untyped parameters.

This can be done in a way too, but only by generating the call to the typed expression from the untyped macro, like:

import macros
macro typedStuff(exp: typed): untyped =
  let impl = exp.getTypeInst
  echo impl.treeRepr

# call typed from untyped
macro moreStuff(cmds: untyped): untyped =
  result = newStmtList()
  let ts = ident"typedStuff"
  for cmd in cmds:
    let id = cmd[0]
    let arg = cmd[1]
    result.add quote do:
      let `id` = `arg`
      `ts` `id`

  echo result.treeRepr

moreStuff:
  x = 1 + 1
  y = "foo" & "bar"

Sorry, for the really bad example. Coming up with macro examples without a use case in mind is hard for me, haha. And I can't wrap my head around whether this can be applied to your macro in some way to make it easier.

@demotomohiro
Copy link
Author

Thank you!
Your example code is nice!
I didn't know we can use bindSym and apply getType* on that symbol.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment