Last active
December 23, 2019 18:10
-
-
Save demotomohiro/43f20fa427f5835065767c08cbd0c88a to your computer and use it in GitHub Desktop.
Macro that automatically generate object type definition from constructor.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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