diff --git a/subfield.nim b/subfield.nim new file mode 100644 index 0000000..e76a81c --- /dev/null +++ b/subfield.nim @@ -0,0 +1,147 @@ +import macros + +type + DotField = object + parents: seq[NimNode] + symbol: NimNode + +iterator findSyms(obj: NimNode): NimNode = + ## Iterate recursively through the symbols + ## attached to the object using a stack + var stack: seq[NimNode] = @[] + stack.add(obj) + + while stack.len() != 0: + let n = stack.pop() + if n.kind == nnkSym: + yield n + else: + for c in n.children: + stack.insert(c, 0) + +proc findFields(obj: NimNode): seq[NimNode] {. compileTime .} = + # ObjectTy + # Empty + # RecList + # Sym "name" + # Sym "surname" + # Sym "age" + result = @[] + + var recList: NimNode + + var tp = obj.getType() + + if tp.kind == nnkBracketExpr: + # This is a ref object: + # + # BracketExpr + # Sym "ref" + # Sym "A:ObjectType" <- getType on this node + # + # A:ObjectType <- then getType on this to get the actual A object type + # + tp = obj.getType()[1].getType() + + # nnkRecList is the "Record List" or field list of the object + recList = tp.findChild(it.kind == nnkRecList) + + if recList.kind != nnkNilLit: + for sym in recList.findSyms(): + result.add(sym) + +proc isNil(dotField: DotField): bool = + ## Check if our custom field object is nil + return dotField.parents.isNil and dotField.symbol.isNil + +proc getNestedField(obj: NimNode, field: NimNode): DotField{.compileTime.} = + ## Get the nested field iteratively using a stack + var stack: seq[DotField] = @[DotField(parents: @[], symbol: obj)] + + var foundField: DotField + let fieldRep = $field + + while stack.len() > 0 and foundField.isNil(): + # Pop an obj off the stack + let dotField = stack[^1] + stack.delete(stack.len - 1) + + var + parents = dotField.parents + currObj = dotField.symbol + + # Put the current symbol in the parents + # of the next symbol + parents.add(currObj) + + for sym in findFields(currObj): + let newDotField = DotField(parents: parents, symbol: sym) + if $sym.toStrLit() == fieldRep: + # We've found our field! + foundField = newDotField + break + else: + # Haven't found it yet, keep iterating + stack.add(newDotField) + + return foundField + +proc transformToDotExpr(foundField: DotField): NimNode = + ## Transform the found field into a dot expression. + ## + ## DotField(parents: @[a, b], symbol: c) + ## + ## turns into: + ## + ## a.b.c + ## + + result = newNimNode(nnkDotExpr) + + # We basically want to turn DotField into an expression, + # so iterate the parents and create a new dot expression + # every 2 parents + for i in 0 ..< foundField.parents.len(): + if result.len() == 2: + result = newDotExpr(result, foundField.parents[i]) + else: + result.add(foundField.parents[i]) + + # If the dot expression has 2 children, it means + # it's already a full dot expression, so create a new + # one with the last symbol as the field being accessed + if result.len() == 2: + result = newDotExpr(result, foundField.symbol) + else: + # otherwise, the dot expression has one free space, + # so just add the last symbol to it + result.add(foundField.symbol) + +macro `.`*(obj: typed, field: untyped): untyped = + ## The anonymous field macro. This allows an object of structure + ## a.b.c.d to access all subfields in one `dot` call. To call `d`, this + ## macro allows simply to reference it via `a.d` + ## + ## called as `a.d` + + let foundField = getNestedField(obj, field) + + if not isNil(foundField): + result = transformToDotExpr(foundField) + else: + # Get the original call + result = callsite() + + if result.kind == nnkCall: + # If this is a proc or method call, change it + # to a proc call syntax because we don't want + # infinite recursion on the macro call + # + # objName.procName() + # + # Call + # Ident !"." + # Sym "objName" <- this is our object + # StrLit procName <- this is our proc + # + result = newCall(ident($result[2]), result[1]) diff --git a/subfield.nimble b/subfield.nimble new file mode 100644 index 0000000..7240e63 --- /dev/null +++ b/subfield.nimble @@ -0,0 +1,15 @@ +# Package + +version = "0.1.0" +author = "Joey Payne" +description = "A sub-field accessor macro for the Nim programming language." +license = "MIT" + +bin = @["subfield"] + +# Dependencies + +requires "nim >= 0.14.0", "einheit >= 0.1.6" + +task tests, "run all tests": + exec "nim c -r tests/tests.nim" diff --git a/tests/nim.cfg b/tests/nim.cfg new file mode 100644 index 0000000..ff5e91c --- /dev/null +++ b/tests/nim.cfg @@ -0,0 +1,2 @@ +--path:"../" +--path:"./" diff --git a/tests/test_subfield.nim b/tests/test_subfield.nim new file mode 100644 index 0000000..a34d93d --- /dev/null +++ b/tests/test_subfield.nim @@ -0,0 +1,63 @@ +import einheit +import subfield + +testSuite SubFieldTests: + method testOneLevel() = + + type + C = object + x: int + B = object + c: C + A = object + b: B + + var c = C(x: 20) + var b = B(c: c) + var a = A(b: b) + + self.check compiles(a.x) + self.check a.x == 20 + + method testTwoLevels() = + type + D = object + y: int + C = object + d: D + B = object + c: C + A = object + b: B + + var d = D(y: 200) + var c = C(d: d) + var b = B(c: c) + var a = A(b: b) + + self.check compiles(a.y) + self.check a.y == 200 + + method testBranch() = + type + D = object + y: int + C = object + x: int + B = object + c: C + d: D + A = object + b: B + + var d = D(y: 200) + var c = C(x: 19) + var b = B(c: c, d: d) + var a = A(b: b) + + self.check compiles(a.y) + self.check a.y == 200 + + self.check compiles(a.x) + self.check a.x == 19 + diff --git a/tests/tests.nim b/tests/tests.nim new file mode 100644 index 0000000..736b9c0 --- /dev/null +++ b/tests/tests.nim @@ -0,0 +1,4 @@ +import einheit +import test_subfield + +runTests()