-
-
Save demotomohiro/8c7c11516df46f009339704bfb713cf6 to your computer and use it in GitHub Desktop.
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 |
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
Vindaar, Thank you!
It is simpler than using random variable name.
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
いいえ!
\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 applygetType*
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.
Thank you!
Your example code is nice!
I didn't know we can use bindSym
and apply getType*
on that symbol.
What I want to do is when I write following code:
objectDef
macro generate following code: