diff --git a/tests/test.nim b/tests/test.nim index e64a2eb..6cdb427 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -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.}, @@ -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.}""" diff --git a/tests/texample.nim b/tests/texample.nim index 4a0ec98..5887045 100644 --- a/tests/texample.nim +++ b/tests/texample.nim @@ -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` diff --git a/tests/texceptions.nim b/tests/texceptions.nim new file mode 100644 index 0000000..5a352ae --- /dev/null +++ b/tests/texceptions.nim @@ -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() + diff --git a/tests/tstreams.nim b/tests/tstreams.nim index 8efe72c..f1dbb94 100644 --- a/tests/tstreams.nim +++ b/tests/tstreams.nim @@ -1,5 +1,5 @@ import ../traitor/streams -import std/unittest +import balls suite "Streams": test "Static Dispatch": @@ -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) diff --git a/traitor.nim b/traitor.nim index 1aaa07f..478f032 100644 --- a/traitor.nim +++ b/traitor.nim @@ -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 @@ -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 @@ -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. @@ -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])) diff --git a/traitor.nimble b/traitor.nimble index 4743396..dab91a0 100644 --- a/traitor.nimble +++ b/traitor.nimble @@ -12,3 +12,5 @@ srcDir = "" requires "nim >= 2.0.0" requires "micros >= 0.1.5" +taskRequires "test", "balls >= 5.0.0" +