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