Create initial subfield macro and tests
This commit is contained in:
parent
f2e46d61a7
commit
ec45c02f72
5 changed files with 231 additions and 0 deletions
147
subfield.nim
Normal file
147
subfield.nim
Normal file
|
|
@ -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])
|
||||
15
subfield.nimble
Normal file
15
subfield.nimble
Normal file
|
|
@ -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"
|
||||
2
tests/nim.cfg
Normal file
2
tests/nim.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
--path:"../"
|
||||
--path:"./"
|
||||
63
tests/test_subfield.nim
Normal file
63
tests/test_subfield.nim
Normal file
|
|
@ -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
|
||||
|
||||
4
tests/tests.nim
Normal file
4
tests/tests.nim
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import einheit
|
||||
import test_subfield
|
||||
|
||||
runTests()
|
||||
Loading…
Add table
Add a link
Reference in a new issue