├── .github ├── build.yml └── workflows │ └── docs.yml ├── .gitignore ├── benchmarks └── bench1.nim ├── readme.md ├── tests ├── test.nim ├── texample.nim ├── texceptions.nim ├── tgeneric.nim ├── tjointhem.nim └── tstreams.nim ├── traitor.nim ├── traitor.nimble └── traitor └── streams.nim /.github/build.yml: -------------------------------------------------------------------------------- 1 | name: Github Actions 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, windows-latest] 9 | 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: jiro4989/setup-nim-action@v1 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | - run: nimble test -y 18 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | nim-version: 'stable' 8 | nim-src: ${{ github.event.repository.name }}.nim 9 | deploy-dir: .gh-pages 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: jiro4989/setup-nim-action@v1 16 | with: 17 | nim-version: ${{ env.nim-version }} 18 | - run: nimble install -Y 19 | - run: nimble doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} 20 | - name: "Copy to index.html" 21 | run: cp ${{ env.deploy-dir }}/${{ github.event.repository.name }}.html ${{ env.deploy-dir }}/index.html 22 | - name: Deploy documents 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ${{ env.deploy-dir }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*.* 3 | !*/ 4 | *.dll 5 | *.exe 6 | *.kate-swap 7 | -------------------------------------------------------------------------------- /benchmarks/bench1.nim: -------------------------------------------------------------------------------- 1 | type 2 | MyObject = ref object of RootObj 3 | data: int 4 | Child = ref object of MyObject 5 | otherData: float 6 | ChildChild = ref object of MyObject 7 | othererData: string 8 | ChildChildChild = ref object of MyObject 9 | hmm: float 10 | ChildChildChildChild = ref object of MyObject 11 | huh: string 12 | 13 | method doThing(obj: MyObject) {.base.} = obj.data += 1 14 | method doThing(obj: Child) = obj.data += 1 15 | method doThing(obj: ChildChild) = obj.data += 1 16 | method doThing(obj: ChildChildChild) = obj.data += 1 17 | method doThing(obj: ChildChildChildChild) = obj.data += 1 18 | 19 | const collSize {.intDefine.} = 1500000 20 | 21 | 22 | import ../traitor 23 | 24 | type 25 | Obj1 = object 26 | data: int 27 | Obj2 = object 28 | data: int 29 | otherData: float 30 | Obj3 = object 31 | data: int 32 | otherData: float 33 | othererData: string 34 | Obj4 = object 35 | data: int 36 | otherData: float 37 | othererData: string 38 | hmm: float 39 | 40 | Obj5 = object 41 | data: int 42 | otherData: float 43 | othererData: string 44 | hmm:float 45 | huh: string 46 | 47 | Thinger = distinct tuple[doThing: proc(_: var Atom) {.nimcall.}] 48 | 49 | proc doThing[T: Obj1 or Obj2 or Obj3 or Obj4 or Obj5](obj: var T) = obj.data += 1 50 | implTrait Thinger 51 | 52 | import criterion 53 | var cfg = newDefaultConfig() 54 | 55 | when defined(useCriterion): 56 | proc `$`(thing: MyObject): string = 57 | if thing of ChildChildChildChild: 58 | $ChildChildChildChild 59 | elif thing of ChildChildChild: 60 | $ChildChildChild 61 | elif thing of ChildChild: 62 | $ChildChild 63 | elif thing of Child: 64 | $Child 65 | else: 66 | $MyObject 67 | 68 | proc `$`(thing: Traitor[Thinger]): string = 69 | if thing of TypedTraitor[Obj1, Thinger]: 70 | $Obj1 71 | elif thing of TypedTraitor[Obj2, Thinger]: 72 | $Obj2 73 | elif thing of TypedTraitor[Obj3, Thinger]: 74 | $Obj3 75 | elif thing of TypedTraitor[Obj4, Thinger]: 76 | $Obj4 77 | else: 78 | $Obj5 79 | 80 | 81 | benchmark cfg: 82 | proc methodDispatch(obj: MyObject) {.measure: [MyObject(), Child(), ChildChild(), ChildChildChild(), ChildChildChildChild()].} = 83 | doThing(obj) 84 | 85 | proc allMethodDispatch(val: openArray[MyObject]) {.measure: [[MyObject(), Child(), ChildChild(), ChildChildChild(), ChildChildChildChild()]].} = 86 | for item in val: 87 | doThing(item) 88 | 89 | proc traitDispatch(val: Traitor[Thinger]) {.measure:[Obj1().toTrait(Thinger), Obj2().toTrait(Thinger), Obj3().toTrait(Thinger), Obj4().toTrait(Thinger), Obj5().toTrait(Thinger)].} = 90 | doThing(val) 91 | 92 | proc allTraitDispatch(val: openArray[Traitor[Thinger]]) {.measure:[[Obj1().toTrait(Thinger), Obj2().toTrait(Thinger), Obj3().toTrait(Thinger), Obj4().toTrait(Thinger), Obj5().toTrait(Thinger)]].} = 93 | for item in val: 94 | doThing(item) 95 | else: 96 | 97 | when defined(useBenchy): 98 | import benchy 99 | else: 100 | import std/[times, monotimes] 101 | template timeit(msg: string, runs: int, body: untyped): untyped = 102 | var 103 | acc: Duration 104 | lowest = Duration.high 105 | highest = Duration.low 106 | 107 | for _ in 0.. x and btn.y < y and (btn.y + btn.h) > y 41 | 42 | proc onClick(btn: Button): string = "Clicked a button" 43 | 44 | proc over(radio: Radio, x, y: int): bool = 45 | radio.r >= (abs(radio.x - x) + abs(radio.y - y)) 46 | 47 | proc onClick(radio: Radio): string = "Clicked a radio" 48 | 49 | emitConverter Button, Clickable # Emit a `converter` for `Button` -> `Traitor[Clickable]` 50 | 51 | type ClickObj[T] = TypedTraitor[T, Clickable] 52 | 53 | var 54 | elements = [ 55 | Traitor[Clickable] Button(w: 10, h: 20), # We can convert directly if we use `emitConvert` 56 | Button(x: 30, y: 30, w: 10, h: 10), 57 | Radio(x: 30, y: 30, r: 10).toTrait(Clickable) # Otherwise we need to convert with `toTrait(trait)` 58 | ] 59 | 60 | assert elements[0].over(3, 3) # We can call procedures as if they're normal 61 | assert elements[1].over(33, 33) 62 | assert elements[2].over(30, 30) 63 | 64 | for i, x in elements: 65 | if x of TypedTraitor[Button, Clickable]: # We can use `of` to check if it's the given type 66 | assert i in [0, 1] 67 | elif x of ClickObj[Radio]: 68 | assert i == 2 69 | 70 | assert elements[2].getData(Radio) == Radio(x: 30, y: 30, r: 10) # We can use `getData` to extract data 71 | elements[2].getData(Radio).x = 0 # It emits a `var T` so it can be mutated 72 | assert elements[2].getData(Radio).x == 0 73 | 74 | let elem = ClickObj[Button](elements[0]) 75 | elem.setProc onClick, proc(arg: Traitor[Clickable]): string = "Hmmmm" 76 | for i, x in elements: 77 | let msg = x.onClick() 78 | case i 79 | of 0: 80 | assert msg == "Hmmmm" 81 | of 1: 82 | assert msg == "Clicked a button" 83 | of 2: 84 | assert msg == "Clicked a radio" 85 | else: 86 | assert false 87 | 88 | for i, elem in elements: 89 | elem.unpackIt: 90 | if i in [0, 1]: 91 | assert it is TypedTraitor[Button, Clickable] 92 | else: 93 | assert it is TypedTraitor[Radio, Clickable] 94 | 95 | Clickable.repackIt(0): 96 | assert It is TypedTraitor[Button, Clickable] 97 | 98 | Clickable.repackIt(1): 99 | assert It is TypedTraitor[Radio, Clickable] 100 | 101 | ``` 102 | 103 | ## Additional information 104 | 105 | Defining `-d:traitorNiceNames` can be used to make the generate procedures have nicer names for debugging. 106 | 107 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import ../traitor 2 | import balls 3 | type 4 | BoundObject* = distinct tuple[ 5 | getBounds: proc (a: var Atom, b: int): (int, int, int, int){.nimcall.}, 6 | doOtherThing: proc(a: Atom): int {.nimcall.}] 7 | DuckObject* = distinct tuple[quack: proc(a: var Atom){.nimcall.}] 8 | 9 | type 10 | MyObj = object 11 | x, y, z, w: int 12 | MyOtherObj = object 13 | a: byte 14 | MyRef = ref object 15 | a: int 16 | 17 | implTrait BoundObject 18 | implTrait DuckObject 19 | 20 | proc getBounds(a: var MyOtherObj, b: int): (int, int, int, int) = (10, 20, 30, 40 * b) 21 | proc doOtherThing(a: MyOtherObj): int = 300 22 | proc quack(a: var MyOtherObj) = a.a = 233 23 | 24 | proc getBounds(a: var MyObj, b: int): (int, int, int, int) = 25 | result = (a.x, a.y, a.z, a.w * b) 26 | a.x = 100 27 | a.y = 300 28 | 29 | proc doOtherThing(a: MyObj): int = a.y * a.z * a.w 30 | 31 | proc quack(a: var MyObj) = discard 32 | 33 | 34 | proc getBounds(a: var MyRef, b: int): (int, int, int, int) = 35 | result = (3, 2, 1, 30) 36 | # It's a ptr to a ptr it seems so this doesnt work 37 | a.a = 300 38 | 39 | proc doOtherThing(a: MyRef): int = 300 40 | 41 | proc quack(a: var MyRef) = a.a = 10 42 | 43 | type 44 | BoundObj[T] = TypedTraitor[T, BoundObject] 45 | DuckObj[T] = TypedTraitor[T, BoundObject] 46 | 47 | emitConverter MyObj, BoundObject 48 | emitConverter MyObj, DuckObject 49 | emitConverter MyOtherObj, BoundObject 50 | emitConverter MyOtherObj, DuckObject 51 | emitConverter MyRef, BoundObject 52 | emitConverter MyRef, DuckObject 53 | 54 | var 55 | valA = MyObj(x: 0, y: 10, z: 30, w: 100) 56 | valB = MyOtherObj() 57 | valC = MyRef() 58 | valD = MyOtherObj() 59 | 60 | type 61 | MyLateType = object 62 | a, b, c: int 63 | PartiallyImplemented = object 64 | 65 | proc getBounds (a: var PartiallyImplemented, b: int): (int, int, int, int) {.nimcall.} = discard 66 | 67 | proc getBounds(a: var MyLateType, b: int): (int, int, int, int) = discard 68 | proc doOtherThing(a: MyLateType): int = discard 69 | proc quack(a: var MyLateType) = a.a = 300 70 | 71 | emitConverter MyLateType, BoundObject 72 | emitConverter MyLateType, DuckObject 73 | 74 | suite "Basic": 75 | test "Basic data logic": 76 | var myData = [Traitor[BoundObject] valA, valB, valC, valD, MyLateType(a: 300)] 77 | check myData[0].getData(MyObj) == MyObj(x: 0, y: 10, z: 30, w: 100) 78 | for x in myData.mitems: 79 | if x of BoundObj[MyObj]: 80 | check x.getData(MyObj).x == 0 81 | check x.getBounds(3) == (0, 10, 30, 300) 82 | let myObj = x.getData(MyObj) 83 | check x.doOtherThing() == myObj.y * myObj.z * myObj.w 84 | elif x of BoundObj[MyRef]: 85 | check x.getBounds(3) == (3, 2, 1, 30) 86 | elif x of BoundObj[MyOtherObj]: 87 | check x.getBounds(3) == (10, 20, 30, 120) 88 | check x.doOtherThing() == 300 89 | 90 | check myData[0].getData(MyObj) == MyObj(x: 100, y: 300, z: 30, w: 100) 91 | check myData[2].getData(MyRef) == valC # Check the ref is the same 92 | 93 | test "Duck testing": 94 | var myQuackyData = [Traitor[DuckObject] valA, valB, valC, valD, MyLateType(a: 50)] 95 | 96 | for x in myQuackyData.mitems: 97 | x.quack() 98 | 99 | check myQuackyData[1].getData(MyOtherObj) == MyOtherObj(a: 233) 100 | myQuackyData[1].getData(MyOtherObj).a = 100 101 | check myQuackyData[1].getData(MyOtherObj) == MyOtherObj(a: 100) # Tests if field access works 102 | check myQuackyData[^1].getData(MyLateType).a == 300 103 | 104 | test "Fail Checks": 105 | type 106 | NotTrait = (int, string) 107 | NotDistinct = tuple[bleh: proc(_: Atom) {.nimcall.}] 108 | check not compiles(implTrait NotTrait) 109 | check not compiles(implTrait NotDistinct) 110 | check not compiles(10.toTrait DuckObject) 111 | check errorCheck[PartiallyImplemented](BoundObject) == """ 112 | 'PartiallyImplemented' failed to match 'BoundObject' due to missing the following procedure(s): 113 | doOtherThing: proc (a: Atom): int {.nimcall.}""" 114 | 115 | check errorCheck[int](DuckObject) == """ 116 | 'int' failed to match 'DuckObject' due to missing the following procedure(s): 117 | quack: proc (a: var Atom) {.nimcall.}""" 118 | -------------------------------------------------------------------------------- /tests/texample.nim: -------------------------------------------------------------------------------- 1 | import ../traitor 2 | type 3 | Clickable = distinct tuple[ # Always use a distinct tuple interface to make it clean and cause `implTrait` requires it 4 | over: proc(a: Atom, x, y: int): bool {.nimcall.}, # Notice we use `Atom` as the first parameter and it's always the only `Atom` 5 | onClick: proc(a: Atom): string {.nimcall.}] 6 | UnimplementedTrait = distinct tuple[ 7 | overloaded: ( # We can add overloads by using a tuple of procs 8 | proc(a: var Atom) {.nimcall.}, 9 | proc(a: Atom, b: int) {.nimcall.}) 10 | ] 11 | 12 | Button = object 13 | x, y, w, h: int 14 | 15 | Radio = object 16 | x, y, r: int 17 | 18 | implTrait Clickable 19 | 20 | proc over(btn: Button, x, y: int): bool = 21 | btn.x < x and (btn.x + btn.w) > x and btn.y < y and (btn.y + btn.h) > y 22 | 23 | proc onClick(btn: Button): string = "Clicked a button" 24 | 25 | proc over(radio: Radio, x, y: int): bool = 26 | radio.r >= (abs(radio.x - x) + abs(radio.y - y)) 27 | 28 | proc onClick(radio: Radio): string = "Clicked a radio" 29 | 30 | emitConverter Button, Clickable # Emit a `converter` for `Button` -> `Traitor[Clickable]` 31 | 32 | type ClickObj[T] = TypedTraitor[T, Clickable] 33 | 34 | var 35 | elements = [ 36 | Traitor[Clickable] Button(w: 10, h: 20), # We can convert directly if we use `emitConvert` 37 | Button(x: 30, y: 30, w: 10, h: 10), 38 | Radio(x: 30, y: 30, r: 10).toTrait(Clickable) # Otherwise we need to convert with `toTrait(trait)` 39 | ] 40 | 41 | assert elements[0].over(3, 3) # We can call procedures as if they're normal 42 | assert elements[1].over(33, 33) 43 | assert elements[2].over(30, 30) 44 | 45 | for i, x in elements: 46 | if x of TypedTraitor[Button, Clickable]: # We can use `of` to check if it's the given type 47 | assert i in [0, 1] 48 | elif x of ClickObj[Radio]: 49 | assert i == 2 50 | 51 | assert elements[2].getData(Radio) == Radio(x: 30, y: 30, r: 10) # We can use `getData` to extract data 52 | elements[2].getData(Radio).x = 0 # It emits a `var T` so it can be mutated 53 | assert elements[2].getData(Radio).x == 0 54 | 55 | let elem = ClickObj[Button](elements[0]) 56 | elem.setProc onClick, proc(arg: Traitor[Clickable]): string = "Hmmmm" 57 | for i, x in elements: 58 | let msg = x.onClick() 59 | case i 60 | of 0: 61 | assert msg == "Hmmmm" 62 | of 1: 63 | assert msg == "Clicked a button" 64 | of 2: 65 | assert msg == "Clicked a radio" 66 | else: 67 | assert false 68 | 69 | for i, elem in elements: 70 | elem.unpackIt: 71 | if i in [0, 1]: 72 | assert it is TypedTraitor[Button, Clickable] 73 | else: 74 | assert it is TypedTraitor[Radio, Clickable] 75 | 76 | Clickable.repackIt(0): 77 | assert It is TypedTraitor[Button, Clickable] 78 | 79 | Clickable.repackIt(1): 80 | assert It is TypedTraitor[Radio, Clickable] 81 | -------------------------------------------------------------------------------- /tests/texceptions.nim: -------------------------------------------------------------------------------- 1 | import ../traitor 2 | import balls 3 | 4 | type RaiseNothing = distinct tuple[doThing: proc(_: Atom){.nimcall, raises: [], noSideEffect.}] 5 | implTrait RaiseNothing 6 | 7 | proc doThing(i: int) = 8 | raise newException(ValueError, "bleh") 9 | 10 | proc doThing(i: float) = discard 11 | 12 | proc doThing(s: string) = echo s 13 | 14 | 15 | 16 | type RaiseValue = distinct tuple[doStuff: proc(_: Atom){.nimcall, raises: [ValueError].}] 17 | 18 | implTrait RaiseValue 19 | 20 | proc doStuff(i: float) = raise (ref ValueError)() 21 | 22 | proc doStuff(s: string) = raise (ref ValueError)() 23 | 24 | proc doStuff(s: bool) = discard 25 | 26 | suite "Proc annotations": 27 | test "raise nothing": 28 | discard 3d.toTrait RaiseNothing 29 | check not compiles(10.toTrait RaiseNothing) 30 | check not compiles("hmm".toTrait RaiseNothing) 31 | 32 | test "raise value": 33 | expect ValueError, 3d.toTrait(RaiseValue).doStuff() 34 | expect ValueError, "".toTrait(RaiseValue).doStuff() 35 | false.toTrait(RaiseValue).doStuff() 36 | 37 | -------------------------------------------------------------------------------- /tests/tgeneric.nim: -------------------------------------------------------------------------------- 1 | import balls 2 | 3 | import ../traitor 4 | type Generic[X] = distinct tuple[doStuff: proc(_: Atom, val: X): string {.nimcall.}] 5 | 6 | implTrait Generic 7 | 8 | proc doStuff[H](i: int, val: H): string = $val 9 | proc doStuff[H](i: string, val: H): string = $val 10 | proc doStuff(i: float, val: string): string = $val 11 | proc doStuff(i: float32, val: string) = discard 12 | 13 | suite "Generic test": 14 | test "Compile Time": 15 | static: 16 | check "oh".toTrait(Generic[int]).doStuff(200) == $200 17 | check 100.toTrait(Generic[int]).doStuff(200) == $200 18 | check "oh".toTrait(Generic[string]).doStuff("what") == "what" 19 | check 100.toTrait(Generic[string]).doStuff("huzuh") == "huzuh" 20 | 21 | test "Runtime": 22 | check "oh".toTrait(Generic[int]).doStuff(200) == $200 23 | check 100.toTrait(Generic[int]).doStuff(200) == $200 24 | check "oh".toTrait(Generic[string]).doStuff("what") == "what" 25 | check 100.toTrait(Generic[string]).doStuff("huzuh") == "huzuh" 26 | 27 | test "Ensure checks work": 28 | check not compiles(10d.toTrait Generic[int]) 29 | check not compiles(10d.toTrait Generic[float]) 30 | check compiles(10d.toTrait Generic[string]) 31 | check not compiles(10f.toTrait Generic[String]) 32 | 33 | 34 | 35 | type Observer*[T] = ref object 36 | subscription: proc(value: T) 37 | error: proc(error: CatchableError) 38 | complete: proc() 39 | 40 | type Observable*[T] = ref object 41 | observers: seq[Observer[T]] 42 | values: seq[T] 43 | complete: bool 44 | 45 | type Subject*[T] = ref object 46 | observers: seq[Observer[T]] 47 | complete: bool 48 | 49 | type Reactable[T] = distinct tuple [ 50 | getObservers: proc(a: Atom): seq[Observer[T]] {.nimcall.} 51 | ] 52 | 53 | implTrait Reactable 54 | proc getObservers[T](reactable: Observable[T]): seq[Observer[T]] = reactable.observers 55 | proc getObservers[T](reactable: Subject[T]): seq[Observer[T]] = reactable.observers 56 | 57 | var subject = Subject[int]().toTrait(Reactable[int]) 58 | 59 | 60 | type 61 | Trait = distinct tuple[ 62 | sendMessage: proc(self: Atom, message: openArray[char]): string 63 | ] 64 | Sender = object 65 | 66 | implTrait Trait 67 | 68 | proc sendMessage(self: Sender, message: openArray[char]): string = $message 69 | 70 | check Sender().toTrait(Trait).sendMessage("Hello, world!") == "['H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!']" 71 | 72 | import pkg/traitor 73 | type 74 | GenericTrait[T] = distinct tuple[ 75 | initialize: proc(self: Atom, userdata: T), 76 | ] 77 | FileSender[T] = object 78 | Message = object 79 | bytes: array[16, char] 80 | 81 | implTrait GenericTrait 82 | 83 | proc initialize(self: FileSender[Message], userdata: Message) = check userData == default(Message) 84 | 85 | FileSender[Message]().toTrait(GenericTrait[Message]).initialize(Message()) 86 | -------------------------------------------------------------------------------- /tests/tjointhem.nim: -------------------------------------------------------------------------------- 1 | import ../traitor 2 | import balls 3 | 4 | 5 | type 6 | Printer = distinct tuple[`$`: proc(_: Atom): string {.nimcall.}] 7 | Writer = distinct tuple[append: proc(_: Atom, s: var string) {.nimcall.}] 8 | 9 | implTrait Printer 10 | implTrait Writer 11 | 12 | proc print(r: AnyTraitor[Printer], _: typedesc[Printer or void] = void): string = 13 | mixin `$` 14 | $r 15 | 16 | proc print(r: AnyTraitor[Writer], _: typedesc[Writer or void] = void) = discard 17 | 18 | proc write(s: var string, r: AnyTraitor[Writer]) = 19 | mixin append 20 | r.append(s) 21 | 22 | type Debug = joinTraits(Printer, Writer) 23 | 24 | implTrait Debug 25 | 26 | proc append(val: auto, s: var string) = s.add $val 27 | 28 | suite "Joined": 29 | test "Basic": 30 | var buff = "" 31 | var a = 10f.toTrait Debug 32 | check a.print(Printer) == $10f 33 | a.print(Writer) 34 | 35 | buff.write(a) 36 | check buff == $10f 37 | buff.setLen(0) 38 | 39 | a = (10, 20, "Hmm").toTrait Debug 40 | buff.write(a) 41 | check buff == $(10, 20, "Hmm") 42 | buff.setLen(0) 43 | 44 | buff.write (10, 20) 45 | check buff == $(10, 20) 46 | 47 | check (10, 20).print(Printer) == $(10, 20) 48 | (10, 20).print(Writer) 49 | -------------------------------------------------------------------------------- /tests/tstreams.nim: -------------------------------------------------------------------------------- 1 | import ../traitor/streams 2 | import balls 3 | 4 | suite "Streams": 5 | test "Static Dispatch": 6 | var ss = StringStream(data: "Hello") # Statically dispatched 7 | check ss.read(array[5, char]) == "Hello" 8 | ss.setPos(0) 9 | check ss.read(5) == "Hello" 10 | discard ss.write(", World!") 11 | ss.setPos(0) 12 | check ss.read(array[13, char]) == "Hello, World!" 13 | ss.setPos(0) 14 | check ss.read(array[13, char]) == "Hello, World!" 15 | 16 | var fs = FileStream.init("/tmp/test.txt", fmReadWrite) 17 | discard fs.write"Hello" 18 | fs.setPos(0) 19 | check fs.read(array[5, char]) == "Hello" 20 | fs.setPos(0) 21 | check fs.read(5) == "Hello" 22 | discard fs.write(", World!") 23 | fs.setPos(0) 24 | check fs.read(array[13, char]) == "Hello, World!" 25 | fs.setPos(0) 26 | check fs.read(array[13, char]) == "Hello, World!" 27 | 28 | test "Dynamic Dispatch": 29 | var strms = [ 30 | StringStream().toTrait StreamTrait, 31 | FileStream.init("/tmp/test2.txt", fmReadWrite).toTrait StreamTrait 32 | ] 33 | for strm in strms.mitems: 34 | discard strm.write "Hello" 35 | strm.setPos(0) 36 | check strm.read(array[5, char]) == "Hello" 37 | strm.setPos(0) 38 | check strm.read(5) == "Hello" 39 | discard strm.write(", World!") 40 | strm.setPos(0) 41 | check strm.read(array[13, char]) == "Hello, World!" 42 | strm.setPos(0) 43 | check strm.read(array[13, char]) == "Hello, World!" 44 | -------------------------------------------------------------------------------- /traitor.nim: -------------------------------------------------------------------------------- 1 | ## This module implements a simple interface over dynamic dispatched traits. 2 | ## It allows one to define the required implementation for a type to match both at runtime and compile time. 3 | ## Enabling the writing of code that does not require inheritance, but still has dynamic dispatch. 4 | 5 | ## Defining `-d:traitorNiceNames` can be used to make the generate procedures have nicer names for debugging. 6 | 7 | 8 | import pkg/micros/introspection 9 | import std/[macros, genasts, strutils, strformat, typetraits, macrocache, tables] 10 | 11 | proc replaceType(tree, arg, inst: NimNode) = 12 | for i, node in tree: 13 | if node.eqIdent arg: 14 | tree[i] = inst 15 | else: 16 | replaceType(node, arg, inst) 17 | 18 | proc instGenTree(trait: NimNode): NimNode = 19 | let trait = 20 | case trait.typeKind 21 | of ntyGenericInst, ntyDistinct, ntyGenericBody: 22 | trait 23 | else: 24 | trait.getTypeInst()[1] 25 | 26 | case trait.kind 27 | of nnkSym: 28 | trait.getTypeImpl()[0] 29 | of nnkBracketExpr: 30 | let trait = 31 | if trait.typeKind == ntyTypeDesc: 32 | trait[1] 33 | else: 34 | trait 35 | 36 | let 37 | typImpl = trait[0].getImpl() 38 | genParams = typImpl[1] 39 | tree = typImpl[^1].copyNimTree() 40 | for i, param in genParams: 41 | tree.replaceType(param, trait[i + 1]) 42 | tree[0] # Skip distinct 43 | else: 44 | trait 45 | 46 | macro isGenericImpl(t: typedesc): untyped = 47 | var t = t.getTypeImpl[^1] 48 | newLit t.kind == nnkSym and (t.kind == nnkBracketExpr or t.getImpl[1].kind == nnkGenericParams) 49 | 50 | proc isGeneric*(t: typedesc): bool = 51 | isGenericImpl(t) 52 | 53 | type Atom* = distinct int ## 54 | ## Default field name to be replaced for all Traits. 55 | ## Should be `distinct void` to prevent instantiation... 56 | 57 | macro forTuplefields(tup: typed, body: untyped): untyped = 58 | result = newStmtList() 59 | let tup = 60 | if tup.kind != nnkTupleConstr: 61 | tup.getTypeInst[^1] 62 | else: 63 | tup 64 | 65 | for x in tup: 66 | let body = body.copyNimTree() 67 | body.insert 0: 68 | genast(x): 69 | type Field {.inject.} = x 70 | result.add nnkIfStmt.newTree(nnkElifBranch.newTree(newLit(true), body)) 71 | result = nnkBlockStmt.newTree(newEmptyNode(), result) 72 | 73 | 74 | proc atomCount(p: typedesc[proc]): int = 75 | forTuplefields(paramsAsTuple(default(p))): 76 | if Field is Atom: 77 | inc result 78 | 79 | proc deAtomProcType(def, trait: NimNode): NimNode = 80 | let typImpl = 81 | if def.kind == nnkProcTy: 82 | def 83 | else: 84 | def[^2] 85 | 86 | result = typImpl.copyNimTree() 87 | result[0][1][^2] = nnkBracketExpr.newTree(ident"Traitor", trait) 88 | 89 | proc desymFields(tree: NimNode) = 90 | for i, node in tree: 91 | if node.kind == nnkIdentDefs: 92 | node[0] = ident($node[0]) 93 | else: 94 | desymFields(node) 95 | 96 | macro emitTupleType*(trait: typedesc): untyped = 97 | ## Exported just to get around generic binding issue 98 | result = nnkTupleConstr.newTree() 99 | let impl = trait.instGenTree() 100 | let trait = trait.getTypeInst[1] 101 | for def in impl: 102 | case def[^2].typeKind 103 | of ntyProc: 104 | result.add deAtomProcType(def, trait) 105 | else: 106 | for prc in def[^2]: 107 | result.add deAtomProcType(prc, trait) 108 | desymFields(result) 109 | 110 | template procCheck(Field: typedesc) = 111 | when Field.paramTypeAt(0) isnot Atom: 112 | {.error: "First parameter should be Atom".} 113 | when Field.atomCount() != 1: 114 | {.error: "Should only be a single atom".} 115 | 116 | template traitCheck(T: typedesc) = 117 | when T isnot distinct or T.distinctBase isnot tuple: 118 | {.error: "Trait should be a distinct tuple".} 119 | forTuplefields(T): 120 | when Field isnot (proc | tuple): 121 | {.error: "Trait fields should be proc or a tuple of procs".} 122 | elif Field is (proc): 123 | procCheck(Field) 124 | else: 125 | forTuplefields(Field): 126 | when Field isnot (proc): 127 | {.error: "Expected tuple of proc for overloaded trait procedures".} 128 | procCheck(Field) 129 | 130 | type 131 | Traitor*[Traits] = ref object of RootObj ## 132 | ## Base Trait object used to ecapsulate the `vtable` 133 | vtable*: typeof(emitTupleType(Traits)) # emitTupleType(Traits) # This does not work cause Nim generics really hate fun. 134 | typeId*: int # Index in the type array 135 | 136 | 137 | TypedTraitor*[T; Traits] {.final, acyclic.} = ref object of Traitor[Traits] ## 138 | ## Typed Trait object has a known data type and can be unpacked 139 | data*: T 140 | 141 | StaticTraitor*[Traits] = concept st ## Allows generic dispatch on types that fit traits 142 | st.toTrait(Traits) is Traitor[Traits] 143 | 144 | AnyTraitor*[Traits] = StaticTraitor[Traits] or Traitor[Traits] ## Allows writing a procedure that operates on both static and runtime. 145 | 146 | InstInfo = typeof(instantiationInfo()) 147 | 148 | 149 | macro getIndex(trait, prc: typed, name: static string): untyped = 150 | let impl = trait.getTypeImpl[1].getImpl[^1][0] 151 | var ind = 0 152 | result = nnkWhenStmt.newTree() 153 | for def in impl: 154 | case def[^2].typeKind 155 | of ntyProc: 156 | if def[0].eqIdent name: 157 | let theType = newCall("typeof", def[^2].deAtomProcType(trait)) 158 | result.add nnkElifBranch.newTree( 159 | infix(prc, "is", theType), 160 | newLit ind) 161 | inc ind 162 | 163 | of ntyTuple: 164 | for traitProc in def[^2]: 165 | if def[0].eqIdent name: 166 | let theType = newCall("typeof", traitProc.deAtomProcType(trait)) 167 | result.add nnkElifBranch.newTree( 168 | infix(prc, "is", theType), 169 | newLit ind) 170 | inc ind 171 | else: 172 | error("Unexpected trait proc", def[^2]) 173 | result.add: 174 | nnkElse.newTree: 175 | genAst(): 176 | {.error: "No proc matches".} 177 | if result[0].kind == nnkElse: 178 | error("No proc matches name: " & name) 179 | 180 | template setProc*[T, Trait](traitor: TypedTraitor[T, Trait], name: untyped, prc: proc) = 181 | ## Allows one to override the vtable for a specific instance 182 | const theProc = prc 183 | traitor.vtable[getIndex(Trait, theProc, astToStr(name))] = theProc 184 | 185 | 186 | proc getData*[T; Traits](tratr: Traitor[Traits], _: typedesc[T]): var T = 187 | ## Converts `tratr` to `TypedTrait[T, Traits]` then access `data` 188 | runnableExamples: 189 | type 190 | MyTrait = distinct tuple[doThing: proc(_: Atom){.nimcall.}] 191 | MyType = object 192 | x: int 193 | implTrait MyTrait 194 | proc doThing(typ: MyType) = discard 195 | let traitObj = MyType(x: 100).toTrait MyTrait 196 | assert traitObj.getData(MyType) == TypedTraitor[MyType, MyTrait](traitObj).data 197 | 198 | 199 | TypedTraitor[T, Traits](tratr).data 200 | 201 | proc genPointerProc(name, origType, instType, origTraitType: NimNode): NimNode = 202 | let procType = origType[0].copyNimTree 203 | when defined(traitorNiceNames): 204 | result = genast(name = ident $name & instType.getTypeImpl[1].repr.multiReplace({"[" : "_", "]": ""})): 205 | proc name() {.nimcall.} = discard 206 | else: 207 | result = genast(name = gensym(nskProc, $name)): 208 | proc name() {.nimcall.} = discard 209 | 210 | let 211 | call = newCall(ident $name) 212 | traitType = nnkBracketExpr.newTree(bindSym"Traitor", origTraitType) 213 | typedTrait = nnkBracketExpr.newTree(bindSym"TypedTraitor", instType, origTraitType) 214 | 215 | result.params[0] = origType[0][0] 216 | 217 | for def in procType[1..^1]: 218 | for _ in def[0..^3]: 219 | let 220 | arg = ident "param" & $(result.params.len - 1) 221 | theTyp = 222 | if result.params.len - 1 == 0: 223 | call.add nnkDotExpr.newTree(nnkCall.newTree(typedTrait, arg), ident"data") 224 | traitType 225 | else: 226 | call.add arg 227 | def[^2] 228 | result.params.add newIdentDefs(arg, theTyp) 229 | 230 | result[^1] = call 231 | result = newStmtList(result, result[0]) 232 | 233 | macro returnTypeMatches(call, typ: typed): untyped = 234 | if call[0][^1].typeKind != ntyNone: 235 | infix(call[0][^1].getType(), "is", typ) 236 | else: 237 | infix(typ, "is", bindSym"void") 238 | 239 | 240 | macro emitPointerProc(trait, instType: typed, err: static bool = false): untyped = 241 | let trait = trait.getTypeImpl[^1] 242 | result = 243 | if err: 244 | nnkBracket.newTree() 245 | else: 246 | nnkTupleConstr.newTree() 247 | let impl = trait.instGenTree() 248 | if err: 249 | for def in impl: 250 | let defImpl = def[^2].getTypeInst 251 | case defImpl.typeKind 252 | of ntyProc: 253 | let prc = genPointerProc(def[0], def[^2], instType, trait) 254 | var 255 | defRetType = def[^2][0][0] 256 | implRet = prc[0][^1] 257 | if defRetType.kind == nnkEmpty: 258 | defRetType = ident"void" 259 | if implRet.kind == nnkEmpty: 260 | implRet = ident"void" 261 | 262 | let def = def.copyNimTree 263 | var hitNimCall = false 264 | 265 | for i, x in def[1][^1]: 266 | if x.kind == nnkIdent and x.eqIdent"nimcall": 267 | if hitNimCall: ## Assume there is only one `nimcall` 268 | def[1][^1].del(i) 269 | break 270 | hitNimCall = true 271 | 272 | result.add: 273 | genast(prc, defRetType, implRet, errorMsg = def.repr): 274 | when not compiles(prc) or (defRetType isnot void and compiles((let x: defRetType = implRet))): 275 | errorMsg 276 | else: 277 | "" 278 | else: 279 | for prc in defImpl: 280 | let 281 | genProc = genPointerProc(def[0], prc, instType, trait) 282 | var 283 | defRetType = prc[0][0] 284 | implRet = genProc[0][^1] 285 | if defRetType.kind == nnkEmpty: 286 | defRetType = ident"void" 287 | if implRet.kind == nnkEmpty: 288 | implRet = ident"void" 289 | 290 | result.add: 291 | genast(genProc, prc, defRetType, name = newLit def[0].repr): 292 | when not compiles(genProc) or not returnTypeMatches(genProc, defRetType): 293 | name & ": " & astToStr(prc) 294 | else: 295 | "" 296 | else: 297 | for def in impl: 298 | let defImpl = def[^2].getTypeInst 299 | case defImpl.typeKind 300 | of ntyProc: 301 | result.add genPointerProc(def[0], def[^2], instType, trait) 302 | else: 303 | for prc in defImpl: 304 | result.add genPointerProc(def[0], prc, instType, trait) 305 | 306 | proc desym(tree: NimNode) = 307 | for i, node in tree: 308 | if node.kind == nnkSym: 309 | tree[i] = ident $node 310 | else: 311 | desym node 312 | 313 | proc genProc(typ, traitType, name: Nimnode, offset: var int): NimNode = 314 | case typ.typeKind 315 | of ntyProc: 316 | let traitType = traitType.copyNimTree() 317 | when defined(traitorNiceNames): 318 | result = genast( 319 | name, 320 | exportedName = newLit "$1_" & traitType.repr.multiReplace({"[": "_", "]": ""}) 321 | ): 322 | proc name*() {.exportc: exportedName.} = discard 323 | else: 324 | result = genast(name = ident $name): 325 | proc name*() = discard 326 | 327 | result.params[0] = typ.params[0].copyNimTree 328 | 329 | let genParams = traitType[1].getImpl()[1] 330 | if genParams.len > 0: 331 | result[2] = nnkGenericParams.newNimNode() 332 | let constraint = nnkBracketExpr.newTree(traitType[1]) 333 | traitType[1] = ident"Arg" 334 | 335 | for typ in genParams: 336 | result[2].add newIdentDefs(ident($typ), newEmptyNode()) 337 | constraint.add ident($typ) 338 | 339 | result[2].add newIdentDefs(traitType[1], constraint) 340 | 341 | let theCall = newCall(newEmptyNode()) 342 | 343 | for i, def in typ.params[1..^1]: # Iterate proc fields 344 | for _ in def[0..^3]: # Iterate names 345 | let 346 | paramName = ident("param" & $(result.params.len - 1)) 347 | theArgTyp = 348 | if result.params.len - 1 == 0: 349 | theCall[0] = genast(offset, paramName): 350 | paramName.vtable[offset] 351 | traitType 352 | else: 353 | def[^2] 354 | 355 | result.params.add newIdentDefs(paramName, theArgTyp) 356 | theCall.add paramName 357 | 358 | desym(result) # Force most body to revaluate 359 | 360 | result[^1] = theCall 361 | inc offset 362 | 363 | of ntyTuple: 364 | result = newStmtList() 365 | for child in typ: 366 | result.add genProc(child, traitType, name, offset) 367 | else: 368 | error("Unexpected type", typ) 369 | 370 | macro genProcs(origTrait: typedesc): untyped = 371 | let trait = origTrait[^1] 372 | var tupl = trait.getTypeInst[^1].getTypeImpl() 373 | if tupl.kind != nnkDistinctTy: 374 | error("Provided trait is not a distinct tuple", tupl) 375 | tupl = trait.instGenTree() 376 | 377 | result = newStmtList() 378 | var offset = 0 379 | for field in tupl: 380 | result.add genProc(field[1], origTrait, field[0], offset) 381 | 382 | macro doError(msg: static string, info: static InstInfo) = 383 | let node = newStmtList() 384 | node.setLineInfo(LineInfo(fileName: info.filename, line: info.line, column: info.column)) 385 | error(msg, node) 386 | 387 | var implementedTraits {.compileTime.}: seq[(NimNode, InstInfo)] 388 | 389 | macro addTrait(t: typedesc, info: static InstInfo) = 390 | case t.kind 391 | of nnkBracketExpr: 392 | error("Expected '" & t[0].repr & "' but got '" & t.repr & "'", t) 393 | of nnkSym: 394 | discard 395 | else: 396 | error("Did not use a type alias for the trait tuple.", t) 397 | implementedTraits.add (t, info) 398 | 399 | macro traitsContain(typ: typedesc): untyped = 400 | result = newLit((false, -1)) 401 | for i, x in implementedTraits: 402 | if x[0] == typ: 403 | return newLit((true, i)) 404 | 405 | macro genbodyCheck(t: typedesc, info: static InstInfo): untyped = 406 | ## Ensures `t` is a genericbody for `implTrait` 407 | if t.getTypeInst[1].typeKind == ntyGenericBody: 408 | let node = newStmtList() 409 | node.setLineInfo(LineInfo(fileName: info.filename, line: info.line, column: info.column)) 410 | error("Cannot use `toTrait` due to lacking generic parameters on '" & t.getTypeInst[1].repr & "'", node) 411 | 412 | proc format(val: InstInfo): string = 413 | fmt"{val.filename}({val.line}, {val.column})" 414 | 415 | const errorMessage = "'$#' failed to match '$#' due to missing the following procedure(s):\n" 416 | 417 | macro unpackItImpl[T](traitor: Traitor[T], table: static CacheSeq, body: untyped) = 418 | result = nnkCaseStmt.newTree(nnkDotExpr.newTree(traitor, ident"typeId")) 419 | var i = 0 420 | for x in table.items: 421 | let elifBody = newStmtList() 422 | elifBody.add: 423 | genast(traitor, x): 424 | let it {.inject.} = TypedTraitor[x, traitor.Traits](traitor) 425 | 426 | elifBody.add body.copyNimTree 427 | result.add nnkOfBranch.newTree(newLit i, elifBody) 428 | inc i 429 | 430 | result.add: 431 | nnkElse.newTree: 432 | genast(traitor): 433 | raise newException(ValueError, "Unexpected ID: " & $traitor.typeId) 434 | 435 | macro repackItImpl(id: int, table: static CacheSeq, trait: typed, body: untyped) = 436 | result = nnkCaseStmt.newTree(id) 437 | var i = 0 438 | for x in table.items: 439 | let elifBody = newStmtList() 440 | elifBody.add: 441 | genast(x, trait): 442 | type It {.inject.} = TypedTraitor[x, trait] 443 | elifBody.add body.copyNimTree 444 | result.add nnkOfBranch.newTree(newLit i, elifBody) 445 | inc i 446 | 447 | result.add: 448 | nnkElse.newTree: 449 | genast(id): 450 | raise newException(ValueError, "Unexpected ID: " & $id) 451 | 452 | template implTrait*(trait: typedesc) = 453 | ## Emits the `vtable` for the given `trait` and a procedure for types to convert to `trait`. 454 | ## It is checked that `trait` is only implemented once so repeated calls error. 455 | runnableExamples: 456 | type MyTrait = distinct tuple[bleh: proc(_: Atom, _: int) {.nimcall.}] 457 | implTrait MyTrait 458 | when not trait.isGeneric(): 459 | traitCheck(trait) 460 | const info {.used.} = instantiationInfo(fullpaths = true) 461 | static: 462 | const (has, ind {.used.}) = traitsContain(trait) 463 | when has: 464 | doError("Trait named '" & $trait & "' was already implemented at: " & implementedTraits[ind][1].format, info) 465 | addTrait(trait, instantiationInfo(fullpaths = true)) 466 | 467 | proc errorCheck[T](traitType: typedesc[trait]): string = 468 | const missing = emitPointerProc(traitType, T, true) 469 | for i, miss in missing: 470 | if miss != "": 471 | if result.len == 0: 472 | result = errorMessage % [$T, $traitType] 473 | result.add miss 474 | if i < missing.high: 475 | result.add "\n" 476 | 477 | const typeTable = CacheSeq "Traitor" & $trait 478 | 479 | proc toTrait*[T; Constraint: trait](val: sink T, traitTyp: typedesc[Constraint]): auto = 480 | ## Converts a type to `traitType` ensuring it implements procedures 481 | ## This creates a `ref` type and moves `val` to it 482 | const procInfo = instantiationInfo(fullPaths = true) 483 | genbodyCheck(traitTyp, procInfo) 484 | const missMsg = errorCheck[T](traitTyp) 485 | when missMsg.len > 0: 486 | doError(missMsg, procInfo) 487 | else: 488 | static: typeTable.add T.getTypeInst() 489 | result = Traitor[traitTyp]( 490 | TypedTraitor[T, traitTyp]( 491 | vtable: emitPointerProc(traitTyp, T), 492 | typeId: static(typeTable.len) - 1, 493 | data: ensureMove val 494 | ) 495 | ) 496 | 497 | template unpackIt*(t: Traitor[trait], body: untyped): untyped = 498 | ## Branches for each known typeId for a trait of `t`. 499 | ## Emits a `it` variable that matches the approriate branch of `t.typeId`. 500 | unpackItImpl(t, typeTable, body) 501 | 502 | template repackIt*(t: typedesc[trait], id: int, body: untyped): untyped = 503 | ## Branches for each known typeId for a trait of `t`. 504 | ## Emits a `It` alias of the `TypedTrait` that matches the approriate branch of `id`. 505 | repackItImpl(id, typeTable, t, body) 506 | 507 | genProcs(Traitor[trait]) 508 | 509 | template emitConverter*(T: typedesc, trait: typedesc) = 510 | ## Emits a converter from `T` to `Traitor[trait]` 511 | ## This allows skipping of `val.toTrait(trait)` 512 | converter convToTrait*(val: sink T): Traitor[trait] {.inject.} = val.toTrait trait 513 | 514 | 515 | proc joinTraitTypes(traits: NimNode): NimNode = 516 | var procs: Table[string, NimNode] 517 | for trait in traits: 518 | for def in trait.getTypeInst[1].getTypeImpl[0]: 519 | if $def[0] notin procs: 520 | procs[$def[0]] = nnkTupleConstr.newTree() 521 | block findIt: 522 | for prc in procs[$def[0]]: 523 | if prc == def[^2]: 524 | break findIt 525 | procs[$def[0]].add def[^2] 526 | result = nnkTupleTy.newTree() 527 | for prc, val in procs: 528 | result.add newIdentDefs(ident $prc, val) 529 | 530 | macro joinTraits*(traits: varargs[typed]): untyped = 531 | result = nnkDistinctTy.newTree joinTraitTypes(traits) 532 | 533 | 534 | when defined(nimdoc): 535 | import traitor/streams 536 | -------------------------------------------------------------------------------- /traitor.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.19" 4 | author = "Jason Beetham" 5 | description = "Trait-like package made without insight" 6 | license = "MIT" 7 | srcDir = "" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 2.0.0" 13 | requires "micros >= 0.1.5" 14 | 15 | taskRequires "test", "https://github.com/disruptek/balls >= 5.0.0" 16 | 17 | 18 | -------------------------------------------------------------------------------- /traitor/streams.nim: -------------------------------------------------------------------------------- 1 | ## A basic `std/streams` like API built using `Traitor` instead of OOP. 2 | 3 | runnableExamples: 4 | ## Static dispatched API 5 | var ss = StringStream(data: "Hello") 6 | assert ss.read(array[5, char]) == "Hello" 7 | ss.setPos(0) 8 | assert ss.read(5) == "Hello" 9 | discard ss.write(", World!") 10 | ss.setPos(0) 11 | assert ss.read(array[13, char]) == "Hello, World!" 12 | ss.setPos(0) 13 | assert ss.read(array[13, char]) == "Hello, World!" 14 | 15 | var fs = FileStream.init("/tmp/test.txt", fmReadWrite) 16 | discard fs.write"Hello" 17 | fs.setPos(0) 18 | assert fs.read(array[5, char]) == "Hello" 19 | fs.setPos(0) 20 | assert fs.read(5) == "Hello" 21 | discard fs.write(", World!") 22 | fs.setPos(0) 23 | assert fs.read(array[13, char]) == "Hello, World!" 24 | fs.setPos(0) 25 | assert fs.read(array[13, char]) == "Hello, World!" 26 | 27 | ## Dynamically dispatched API 28 | var strms = [StringStream().toTrait StreamTrait, FileStream.init("/tmp/test2.txt", fmReadWrite).toTrait StreamTrait] 29 | for strm in strms.mitems: 30 | discard strm.write "Hello" 31 | strm.setPos(0) 32 | assert strm.read(array[5, char]) == "Hello" 33 | strm.setPos(0) 34 | assert strm.read(5) == "Hello" 35 | discard strm.write(", World!") 36 | strm.setPos(0) 37 | assert strm.read(array[13, char]) == "Hello, World!" 38 | strm.setPos(0) 39 | assert strm.read(array[13, char]) == "Hello, World!" 40 | 41 | import ../traitor 42 | import std/typetraits 43 | 44 | type 45 | StreamTrait* = distinct tuple[ 46 | readData: proc(_: var Atom, dest: pointer, len: int): int {.nimcall.}, 47 | writeData: proc(_: var Atom, toWrite: pointer, len: int): int {.nimcall.}, 48 | setPos: proc(_: var Atom, pos: int) {.nimcall.}, 49 | getPos: proc(_: Atom): int {.nimcall.}, 50 | atEnd: proc(_: Atom): bool {.nimcall.} 51 | ] ## Any stream must match this trait to be used by this API. 52 | Stream* = AnyTraitor[StreamTrait] ## 53 | ## Allows static dispatch where possible, but also dynamic dispatch when converted to a `Traitor[Stream]` 54 | 55 | PrimitiveBase* = concept pb 56 | pb.distinctBase is PrimitiveAtom 57 | PrimitiveAtom* = SomeOrdinal or SomeFloat or enum or bool or char or PrimitiveBase or set ## 58 | ## Built in value types that can be copied by memory 59 | 60 | proc onlyPrimitives*(val: typedesc[PrimitiveAtom]) = 61 | ## All PrimitiveAtoms are safe to stream directly. 62 | doAssert true 63 | 64 | proc onlyPrimitives*[Idx, T](val: typedesc[array[Idx, T]])= 65 | ## Procedure to ensure `array`s only are made of prototypes 66 | onlyPrimitives(T) 67 | 68 | proc onlyPrimitives(obj: typedesc[object or tuple]) = 69 | ## Procedure to ensure `object`s only are made of prototypes 70 | for field in default(obj).fields: 71 | onlyPrimitives(typeof(field)) 72 | 73 | type 74 | Primitive* = concept type P ## Any type that is `onlyPrimitives(T) is true` 75 | onlyPrimitives(P) 76 | 77 | implTrait StreamTrait 78 | 79 | proc read*(strm: var Stream, T: typedesc[Primitive]): T = 80 | ## Reads the exact amount from `strm` 81 | ## Raises if it fails to read fully 82 | mixin readData 83 | let read = strm.readData(result.addr, sizeof(T)) 84 | if read != sizeof(T): 85 | raise newException(ValueError, "Did not fully read data, only read: " & $read) 86 | 87 | proc read*(strm: var Stream, maxAmount: int): string = 88 | ## Reads upto `maxAmount` from `strm` 89 | mixin readData 90 | result = newString(maxAmount) 91 | result.setLen(strm.readData(result[0].addr, maxAmount)) 92 | 93 | proc write*(strm: var Stream, data: Primitive): int = 94 | ## Writes `data` to `strm` 95 | mixin writeData 96 | strm.writeData(data.addr, sizeof(data)) 97 | 98 | proc write*(strm: var Stream, data: string): int = 99 | ## Overload for `string` that writes `data`'s data to `strm` 100 | mixin writeData 101 | strm.writeData(data[0].addr, data.len) 102 | 103 | type 104 | StringStream* = object 105 | pos: int 106 | data*: string 107 | 108 | proc atEnd*(ss: StringStream): bool = ss.pos >= ss.data.len 109 | 110 | proc readData*(ss: var StringStream, dest: pointer, amount: int): int = 111 | if amount == 0 or ss.atEnd: 112 | 0 113 | elif amount <= ss.data.len - ss.pos: 114 | copyMem(dest, ss.data[ss.pos].addr, amount) 115 | ss.pos += amount 116 | amount 117 | else: 118 | copyMem(dest, ss.data[ss.pos].addr, ss.data.len - ss.pos) 119 | ss.pos = ss.data.len 120 | ss.data.len - ss.pos 121 | 122 | proc writeData*(ss: var StringStream, dest: pointer, amount: int): int = 123 | if amount + ss.pos > ss.data.len: 124 | ss.data.setLen(amount + ss.pos) 125 | copyMem(ss.data[ss.pos].addr, dest, amount) 126 | ss.pos += amount 127 | amount 128 | 129 | proc setPos*(ss: var StringStream, pos: int) = ss.pos = pos 130 | proc getPos*(ss: var StringStream): int = ss.pos 131 | 132 | type 133 | FileStream* = object 134 | file: File 135 | 136 | proc init*(_: typedesc[FileStream], path: string, mode: FileMode = fmRead): FileStream = 137 | FileStream(file: open(path, mode)) 138 | 139 | proc `=destroy`(fs: FileStream) = 140 | close(fs.file) 141 | 142 | proc readData*(fs: var FileStream, dest: pointer, amount: int): int = 143 | fs.file.readBuffer(dest, amount) 144 | 145 | proc writeData*(fs: var FileStream, data: pointer, amount: int): int = 146 | fs.file.writeBuffer(data, amount) 147 | 148 | proc atEnd*(fs: var FileStream): bool = fs.file.endOfFile() 149 | 150 | proc setPos*(fs: var FileStream, pos: int) = fs.file.setFilePos(pos) 151 | proc getPos*(fs: var FileStream): int = int fs.file.getFilePos() 152 | --------------------------------------------------------------------------------