Skip to content

Instantly share code, notes, and snippets.

@demotomohiro
Created November 20, 2019 05:40
Show Gist options
  • 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

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