Skip to content

Commit

Permalink
expand tests and reintroduce mismatch error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
beef331 committed Feb 5, 2024
1 parent 8f25705 commit c27fb3c
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 49 deletions.
85 changes: 54 additions & 31 deletions tests/test.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ../traitor
import std/unittest
import balls
type
BoundObject* = distinct tuple[
getBounds: proc (a: var Atom, b: int): (int, int, int, int){.nimcall.},
Expand Down Expand Up @@ -57,40 +57,63 @@ var
valC = MyRef()
valD = MyOtherObj()

type MyLateType = object
a, b, c: int
type
MyLateType = object
a, b, c: int
PartiallyImplemented = object

proc getBounds (a: var PartiallyImplemented, b: int): (int, int, int, int) {.nimcall.} = discard

proc getBounds(a: var MyLateType, b: int): (int, int, int, int) = discard
proc doOtherThing(a: MyLateType): int = discard
proc quack(a: var MyLateType) = a.a = 300

emitConverter MyLateType, BoundObject
emitConverter MyLateType, DuckObject

test "Basic data logic":
var myData = [Traitor[BoundObject] valA, valB, valC, valD, MyLateType(a: 300)]
check myData[0].getData(MyObj) == MyObj(x: 0, y: 10, z: 30, w: 100)
for x in myData.mitems:
if x of BoundObj[MyObj]:
check x.getData(MyObj).x == 0
check x.getBounds(3) == (0, 10, 30, 300)
let myObj = x.getData(MyObj)
check x.doOtherThing() == myObj.y * myObj.z * myObj.w
elif x of BoundObj[MyRef]:
check x.getBounds(3) == (3, 2, 1, 30)
elif x of BoundObj[MyOtherObj]:
check x.getBounds(3) == (10, 20, 30, 120)
check x.doOtherThing() == 300

check myData[0].getData(MyObj) == MyObj(x: 100, y: 300, z: 30, w: 100)
check myData[2].getData(MyRef) == valC # Check the ref is the same

test "Duck testing":
var myQuackyData = [Traitor[DuckObject] valA, valB, valC, valD, MyLateType(a: 50)]

for x in myQuackyData.mitems:
x.quack()

check myQuackyData[1].getData(MyOtherObj) == MyOtherObj(a: 233)
myQuackyData[1].getData(MyOtherObj).a = 100
check myQuackyData[1].getData(MyOtherObj) == MyOtherObj(a: 100) # Tests if field access works
check myQuackyData[^1].getData(MyLateType).a == 300
suite "Basic":
test "Basic data logic":
var myData = [Traitor[BoundObject] valA, valB, valC, valD, MyLateType(a: 300)]
check myData[0].getData(MyObj) == MyObj(x: 0, y: 10, z: 30, w: 100)
for x in myData.mitems:
if x of BoundObj[MyObj]:
check x.getData(MyObj).x == 0
check x.getBounds(3) == (0, 10, 30, 300)
let myObj = x.getData(MyObj)
check x.doOtherThing() == myObj.y * myObj.z * myObj.w
elif x of BoundObj[MyRef]:
check x.getBounds(3) == (3, 2, 1, 30)
elif x of BoundObj[MyOtherObj]:
check x.getBounds(3) == (10, 20, 30, 120)
check x.doOtherThing() == 300

check myData[0].getData(MyObj) == MyObj(x: 100, y: 300, z: 30, w: 100)
check myData[2].getData(MyRef) == valC # Check the ref is the same

test "Duck testing":
var myQuackyData = [Traitor[DuckObject] valA, valB, valC, valD, MyLateType(a: 50)]

for x in myQuackyData.mitems:
x.quack()

check myQuackyData[1].getData(MyOtherObj) == MyOtherObj(a: 233)
myQuackyData[1].getData(MyOtherObj).a = 100
check myQuackyData[1].getData(MyOtherObj) == MyOtherObj(a: 100) # Tests if field access works
check myQuackyData[^1].getData(MyLateType).a == 300

test "Fail Checks":
type
NotTrait = (int, string)
NoTDistinct = tuple[bleh: proc(_: Atom) {.nimcall.}]


check not compiles(implTrait NotTrait)
check not compiles(implTrait NotDistinct)
check not compiles(10.toTrait DuckObject)
check errorCheck[PartiallyImplemented](BoundObject) == """
'PartiallyImplemented' failed to match 'BoundObject' due to missing the following procedure(s):
proc (a: Atom): int {.nimcall.}"""

check errorCheck[int](DuckObject) == """
'int' failed to match 'DuckObject' due to missing the following procedure(s):
proc (a: var Atom) {.nimcall.}"""
2 changes: 1 addition & 1 deletion tests/texample.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import traitor
import ../traitor
type
Clickable = distinct tuple[ # Always use a distinct tuple interface to make it clean and cause `implTrait` requires it
over: proc(a: Atom, x, y: int): bool {.nimcall.}, # Notice we use `Atom` as the first parameter and it's always the only `Atom`
Expand Down
36 changes: 36 additions & 0 deletions tests/texceptions.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ../traitor
import balls

type RaiseNothing = distinct tuple[doThing: proc(_: Atom){.nimcall, raises: [], noSideEffect.}]
implTrait RaiseNothing

proc doThing(i: int) =
raise newException(ValueError, "bleh")

proc doThing(i: float) = discard

proc doThing(s: string) = echo s



type RaiseValue = distinct tuple[doStuff: proc(_: Atom){.nimcall, raises: [ValueError].}]

implTrait RaiseValue

proc doStuff(i: float) = raise (ref ValueError)()

proc doStuff(s: string) = raise (ref ValueError)()

proc doStuff(s: bool) = discard

suite "Proc annotations":
test "raise nothing":
discard 3d.toTrait RaiseNothing
check not compiles(10.toTrait RaiseNothing)
check not compiles("hmm".toTrait RaiseNothing)

test "raise value":
expect ValueError, 3d.toTrait(RaiseValue).doStuff()
expect ValueError, "".toTrait(RaiseValue).doStuff()
false.toTrait(RaiseValue).doStuff()

7 changes: 5 additions & 2 deletions tests/tstreams.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ../traitor/streams
import std/unittest
import balls

suite "Streams":
test "Static Dispatch":
Expand All @@ -26,7 +26,10 @@ suite "Streams":
check fs.read(array[13, char]) == "Hello, World!"

test "Dynamic Dispatch":
var strms = [StringStream().toTrait StreamTrait, FileStream.init("/tmp/test2.txt", fmReadWrite).toTrait StreamTrait]
var strms = [
StringStream().toTrait StreamTrait,
FileStream.init("/tmp/test2.txt", fmReadWrite).toTrait StreamTrait
]
for strm in strms.mitems:
discard strm.write "Hello"
strm.setPos(0)
Expand Down
72 changes: 57 additions & 15 deletions traitor.nim
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ type
else:
vtable*: ptr typeof(emitTupleType(Traits)) # ptr emitTupleType(Traits) # This does not work cause Nim generics really hate fun.

TypedTraitor*[T; Traits: ValidTraitor] {.final.} = ref object of Traitor[Traits] ##
TypedTraitor*[T; Traits: ValidTraitor] {.final, acyclic.} = ref object of Traitor[Traits] ##
## Typed Trait object has a known data type and can be unpacked
data*: T

Expand Down Expand Up @@ -127,17 +127,43 @@ proc genPointerProc(name, origType, instType, origTraitType: NimNode): NimNode =
result[^1] = call
result = newStmtList(result, result[0])

macro emitPointerProc(trait, instType: typed): untyped =
result = nnkTupleConstr.newTree()
let impl = trait.getImpl[^1][0]
for def in impl:
let defImpl = def[^2].getTypeInst
case defImpl.typeKind
of ntyProc:
result.add genPointerProc(def[0], def[^2], instType, trait)
macro emitPointerProc(trait, instType: typed, err: static bool = false): untyped =
result =
if err:
nnkBracket.newTree()
else:
for prc in defImpl:
result.add genPointerProc(def[0], prc, instType, trait)
nnkTupleConstr.newTree()
let impl = trait.getImpl[^1][0]
if err:
for def in impl:
let defImpl = def[^2].getTypeInst
case defImpl.typeKind
of ntyProc:
let prc = genPointerProc(def[0], def[^2], instType, trait)
result.add:
genast(prc, typ = def[^2]):
when not compiles(prc):
astToStr(typ)
else:
""
else:
for prc in defImpl:
let genProc = genPointerProc(def[0], prc, instType, trait)
result.add:
genast(genProc, prc):
when not compiles(genProc):
astToStr(prc)
else:
""
else:
for def in impl:
let defImpl = def[^2].getTypeInst
case defImpl.typeKind
of ntyProc:
result.add genPointerProc(def[0], def[^2], instType, trait)
else:
for prc in defImpl:
result.add genPointerProc(def[0], prc, instType, trait)

proc genProc(typ, traitType, name: Nimnode, offset: var int): NimNode =
case typ.typeKind
Expand Down Expand Up @@ -212,6 +238,8 @@ macro traitsContain(typ: typedesc): untyped =
proc format(val: InstInfo): string =
fmt"{val.filename}({val.line}, {val.column})"

const errorMessage = "'$#' failed to match '$#' due to missing the following procedure(s):\n"

template implTrait*(trait: typedesc[ValidTraitor]) =
## Emits the `vtable` for the given `trait` and a procedure for types to convert to `trait`.
## It is checked that `trait` is only implemented once so repeated calls error.
Expand All @@ -225,12 +253,26 @@ template implTrait*(trait: typedesc[ValidTraitor]) =
doError("Trait named '" & $trait & "' was already implemented at: " & implementedTraits[ind][1].format, info)
addTrait(trait, instantiationInfo(fullpaths = true))

proc errorCheck[T](_: typedesc[trait]): string =
const missing = emitPointerProc(trait, T, true)
for i, miss in missing:
if miss != "":
if result.len == 0:
result = errorMessage % [$T, $trait]
result.add miss
if i < missing.high:
result.add "\n"

proc toTrait*[T](val: sink T, _: typedesc[trait]): Traitor[trait] =
when defined(traitor.fattraitors):
TypedTraitor[T, trait](vtable: static(emitPointerProc(trait, T)), data: ensureMove val)
const missMsg = errorCheck[T](trait)
when missMsg.len > 0:
doError(missMsg, instantiationInfo(fullPaths = true))
else:
let vtable {.global.} = static(emitPointerProc(trait, T))
TypedTraitor[T, trait](vtable: vtable.addr, data: ensureMove val)
when defined(traitor.fattraitors):
TypedTraitor[T, trait](vtable: static(emitPointerProc(trait, T)), data: ensureMove val)
else:
let vtable {.global.} = static(emitPointerProc(trait, T))
TypedTraitor[T, trait](vtable: vtable.addr, data: ensureMove val)

{.checks: off.}
genProcs(default(Traitor[trait]))
Expand Down
2 changes: 2 additions & 0 deletions traitor.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ srcDir = ""
requires "nim >= 2.0.0"
requires "micros >= 0.1.5"

taskRequires "test", "balls >= 5.0.0"

0 comments on commit c27fb3c

Please sign in to comment.