Skip to content

Instantly share code, notes, and snippets.

@demotomohiro
Last active December 23, 2019 18:10
Show Gist options
  • Save demotomohiro/43f20fa427f5835065767c08cbd0c88a to your computer and use it in GitHub Desktop.
Save demotomohiro/43f20fa427f5835065767c08cbd0c88a to your computer and use it in GitHub Desktop.
Macro that automatically generate object type definition from constructor.
import macros
macro tupleCtorToObjectDef(name: static[string];
isExported: static[bool];
isRef: static[bool];
procDef: typed{nkProcDef}): untyped =
func getObjType(name: string; isExported: bool; isRef: bool; node: NimNode): NimNode =
if node.kind == nnkAsgn and node[0].strVal == "result":
node[1].expectKind nnkTupleConstr
var recList = newNimNode(nnkRecList)
for n in node[1]:
n.expectKind nnkExprColonExpr
n[1].expectKind nnkTupleConstr
let
fieldIdent = n[0].strVal.ident
fieldType = n[1][0].getTypeInst
if n[1].len == 1:
recList.add newIdentDefs(fieldIdent, fieldType)
else:
recList.add newIdentDefs(postfix(fieldIdent, "*"), fieldType)
let objNameIdent = if isExported:
postfix(name.ident, "*")
else:
name.ident
if isRef:
let typDef = quote do:
type `objNameIdent` = ref object
typDef[0][2][0][2] = recList
return typDef
else:
let typDef = quote do:
type `objNameIdent` = object
typDef[0][2][2] = recList
return typDef
else:
return nil
var objType = getObjType(name, isExported, isRef, procDef.body)
if objType == nil:
for n in procDef.body:
objType = getObjType(name, isExported, isRef, n)
if objType != nil:
break
objType
macro objectDef*(isRef: static[bool]; procDef: untyped{nkProcDef}): untyped =
## Create a object type definition from a constructor proc.
## This macro works by adding `objectDef` pragma to your constructor proc.
## It read assignments and var statements under `fieldDef` and
## create an object type that has corresponding fields.
## New object type have same name to the input proc.
##
## When `isRef == false`, object type is defined and
## `init` prefix is added to the input proc name.
## When `isRef == true`, ref object type is defined and
## `new` prefix is added to the input proc name.
## When the input proc is exported, new object type is also exported.
##
## Limitations:
## - Only assignments, var statements and comments are allowed under `fieldDef`.
## - Generic proc is not supported.
## - You can use `fieldDef` only once in a input proc.
## - `fieldDef` must be placed under input proc and cannot be placed under other statments.
runnableExamples:
proc Foo() {.objectDef(false).} =
fieldDef:
x = 1 + 2
y = "foo " & "bar"
let a = initFoo()
doAssert a == Foo(x: 3, y: "foo bar")
proc isExportedIdent(n: NimNode): bool =
n.kind == nnkPostfix and n.len == 2 and n[0].strVal == "*"
var dummyProc = procDef.copy
dummyProc.name = genSym(nskProc)
dummyProc.params[0] = ident"auto"
let
objName = procDef.name.strVal
isExported = isExportedIdent(procDef[0])
var newCtor = procDef.copy
newCtor.name = ident((if isRef: "new" else: "init") & objName)
if isExported:
newCtor[0] = postfix(newCtor.name, "*")
newCtor.params[0] = objName.ident
var foundFieldDef: bool
for i, n in procDef.body.pairs:
if n.kind == nnkCall and n[0].strVal == "fieldDef":
if foundFieldDef:
error "You may not use multiple fieldDef in a proc body"
foundFieldDef = true
var
tupleCtor = nnkPar.newNimNode
objCtor = nnkObjConstr.newTree(objName.ident)
n.expectLen 2
n[1].expectKind nnkStmtList
for m in n[1]:
m.expectKind {nnkAsgn, nnkVarSection, nnkCommentStmt}
if m.kind == nnkAsgn:
m[0].expectKind nnkIdent
tupleCtor.add newColonExpr(m[0], nnkTupleConstr.newTree(m[1]))
objCtor.add newColonExpr(m[0], m[1])
elif m.kind == nnkVarSection:
for l in m:
let initVal = l.last or newCall(bindSym"default", l[^2])
for k in 0..<(l.len - 2):
if l[k].isExportedIdent:
tupleCtor.add newColonExpr(l[k][1], nnkTupleConstr.newTree(initVal, nnkTupleConstr.newNimNode()))
objCtor.add newColonExpr(l[k][1], initVal)
else:
tupleCtor.add newColonExpr(l[k], nnkTupleConstr.newTree(initVal))
objCtor.add newColonExpr(l[k], initVal)
#echo tupleCtor.treeRepr
dummyProc.body[i] = newAssignment(ident"result", tupleCtor)
newCtor.body[i] = newAssignment(ident"result", objCtor)
if not foundFieldDef:
warning "There is no fieldDef in your proc with objectDef macro"
newStmtList(
newCall(
bindSym"tupleCtorToObjectDef",
objName.newLit,
isExported.newLit,
isRef.newLit,
dummyProc),
newCtor)
when isMainModule:
import sets, sugar
macro isItExported(p: typed): bool =
return isExported(if p.kind == nnkDotExpr: p[1] else: p).newLit
proc Test1() {.objectDef(false).} =
fieldDef:
a = 1
doAssert not isItExported(Test1)
doAssert not isItExported(initTest1)
let test1 = initTest1()
doAssert test1 == Test1(a: 1)
proc Test1Ref() {.objectDef(true).} =
fieldDef:
a = "xyz"
doAssert not isItExported(Test1Ref)
doAssert not isItExported(newTest1Ref)
let test1Ref = newTest1Ref()
doAssert test1Ref[] == Test1Ref(a: "xyz")[]
var test1Refv: Test1Ref
doAssert test1Refv == nil
proc Foo*() {.objectDef(false).} =
fieldDef:
## Fields of object type Foo.
x = 1 + 2
y = "foo " & "bar"
z = 7.0
let a = initFoo()
doAssert isItExported(Foo)
doAssert isItExported(initFoo)
doAssert a == Foo(x: 3, y: "foo bar", z: 7.0)
proc FooRef*() {.objectDef(true).} =
let a = 1
fieldDef:
x = a
y = ""
z = initFoo()
doAssert isItExported(FooRef)
doAssert isItExported(newFooRef)
let a2 = newFooRef()
doAssert a2[] == FooRef(x: 1, y: "", z: Foo(x: 3, y: "foo bar", z: 7.0))[]
proc Bar(arg: int) {.objectDef(true).} =
var v = 1
let ary = ['a', 'b', 'c']
fieldDef:
a = v + 2
b = initHashSet[int](arg)
c = toHashSet(ary)
d = 1.0
e = [3, 4]
f = @["abc", "def"]
dump(result.a)
dump(result.b)
let b = newBar(4)
doAssert not isItExported(Bar)
doAssert not isItExported(newBar)
doAssert b.a == 3
doAssert b.b is HashSet[int]
doAssert b.b.len == 0
doAssert b.c == toHashSet(['a', 'b', 'c'])
doAssert b.d == 1.0
doAssert b.e == [3, 4]
doAssert b.f == @["abc", "def"]
proc TestVar*(arg: string) {.objectDef(false).} =
var x, y = 2
fieldDef:
a = 1
var
b* = x
c: int
var
e, f = y + 1
g, h = arg
i, j, k: string
l*, m: array[1, char]
n: Foo
o*: Bar
p, q* = (1,)
let testVar* = initTestVar("TestVar")
doAssert isItExported(TestVar)
doAssert isItExported(initTestVar)
doAssert not isItExported(testVar.a)
doAssert isItExported(testVar.b)
doAssert not isItExported(testVar.c)
doAssert not isItExported(testVar.k)
doAssert isItExported(testVar.l)
doAssert not isItExported(testVar.m)
doAssert isItExported(testVar.o)
doAssert not isItExported(testVar.p)
doAssert isItExported(testVar.q)
doAssert testVar == TestVar(a: 1, b: 2, c: 0, e: 3, f: 3, g: "TestVar", h: "TestVar", i: "", j: "", k: "", l: ['\0'], m: ['\0'], n: Foo(), o: nil, p: (1,), q: (1,))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment