diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3aa2447 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: metagn +custom: https://www.buymeacoffee.com/metagn diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..3031f02 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,26 @@ +name: dirtydeeds + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: jiro4989/setup-nim-action@v1 + + - name: install nimbleutils + run: nimble install -y https://github.com/metagn/nimbleutils@#HEAD + + - name: install dependencies + run: nimble install -y + + - name: run tests + run: nimble tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c138f01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.exe +*.dll diff --git a/README.md b/README.md new file mode 100644 index 0000000..98ab8b8 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# dirtydeeds + +Quick and dirty partial application of calls with possible typed arguments. + +```nim +import dirtydeeds, sequtils + +assert @[1, 2, 3].map(deed _ * 7) == @[7, 14, 21] +assert @["A", "B", "C"].map(deed "foo" & (_: string)) == @["fooA", "fooB", "fooC"] +assert @['a', 'f', 'A', '0', 'c'].filter(deed contains({'a'..'z'}, _)) == @['a', 'f', 'c'] +``` + +More uses in tests. Note that this is currently only for partial application, +things like `_ + (_ - 1)` will not work. diff --git a/dirtydeeds.nimble b/dirtydeeds.nimble new file mode 100644 index 0000000..9520078 --- /dev/null +++ b/dirtydeeds.nimble @@ -0,0 +1,21 @@ +# Package + +version = "0.1.0" +author = "metagn" +description = "macro for partially applied calls" +license = "MIT" +srcDir = "src" + + +# Dependencies + +requires "nim >= 1.0.0" + +when (compiles do: import nimbleutils): + import nimbleutils + +task tests, "run tests for multiple backends": + when declared(runTests): + runTests(backends = {c, js, nims}) + else: + echo "tests task not implemented, need nimbleutils" diff --git a/src/dirtydeeds.nim b/src/dirtydeeds.nim new file mode 100644 index 0000000..949fd1a --- /dev/null +++ b/src/dirtydeeds.nim @@ -0,0 +1,112 @@ +import macros + +proc extractParam(node, defaultType: NimNode, count: int): NimNode = + result = nil + case node.kind + of nnkCallKinds, nnkObjConstr: + # _(a) means param named a + if node.len == 2 and node[0].kind == nnkIdent and node[0].eqIdent"_": + result = newIdentDefs(node[1], defaultType) + result.copyLineInfo(node) + of nnkExprEqExpr, nnkAsgn: + # default value + result = extractParam(node[0], defaultType, count) + if not result.isNil: + result[2] = node[1] + of nnkExprColonExpr: + # type + result = extractParam(node[0], defaultType, count) + if not result.isNil: + result[1] = node[1] + of nnkIdent: + if node.eqIdent"_": + result = newIdentDefs(ident("_" & $count), defaultType) + result.copyLineInfo(node) + of nnkPar, nnkTupleConstr: + if node.len == 1 and node[0].kind notin {nnkPar, nnkTupleConstr}: + result = extractParam(node[0], defaultType, count) + else: discard + +proc impl(node: NimNode): NimNode = + const + paramPos = 3 + genericPos = 2 + bodyPos = ^1 + if node.kind in RoutineNodes: + result = node + else: + result = newProc( + procType = nnkLambda, + body = node) + result.copyLineInfo(node) + let defaultType = + if result.kind in {nnkTemplateDef, nnkMacroDef}: + ident"untyped" + else: + ident"auto" + if result[paramPos][0].kind == nnkEmpty: + result[paramPos][0] = defaultType + var paramCount = 0 + if result[bodyPos].kind in {nnkStmtList, nnkStmtListExpr}: + var i = 0 + while i < result[bodyPos].len - 1: + let e = result[bodyPos][i] + let p = extractParam(e, defaultType, paramCount) + if not p.isNil: + result[paramPos].add(p) + inc paramCount + result[bodyPos].del(i) + else: + inc i + else: + let old = result[bodyPos] + result[bodyPos] = newNimNode(nnkStmtListExpr, old) + result[bodyPos].add(old) + let + body = result[bodyPos] + callPos = body.len - 1 + let call = body[callPos] + case call.kind + of nnkCallKinds, nnkObjConstr, nnkBracketExpr, nnkCurlyExpr, + nnkPar, nnkTupleConstr, nnkBracket, nnkCurly: + if call.kind in nnkCallKinds + {nnkObjConstr}: + let callee = call[0] + if callee.kind == nnkBracketExpr: + # maybe generic params + var genericParams: seq[NimNode] + var i = 1 + while i < callee.len: + let p = extractParam(callee[i], newEmptyNode(), paramCount) + if not p.isNil: + genericParams.add(p) + inc paramCount + callee.del(i) + else: + inc i + if genericParams.len != 0: + if result[genericPos].kind == nnkEmpty: + result[genericPos] = newNimNode(nnkGenericParams, callee) + result[genericPos].add(genericParams) + if callee.len == 1: + call[0] = callee[0] + for i in 0 ..< call.len: + let p = extractParam(call[i], defaultType, paramCount) + if not p.isNil: + result[paramPos].add(p) + inc paramCount + call[i] = p[0] + if call.kind == nnkObjConstr and + call.len > 1 and call[1].kind != nnkExprColonExpr: + body[callPos] = newNimNode(nnkCall, call) + for a in call: body[callPos].add(a) + of nnkDotExpr, nnkDerefExpr: + let p = extractParam(call[0], defaultType, paramCount) + if not p.isNil: + result[paramPos].add(p) + inc paramCount + call[0] = p[0] + else: + warning("unsupported deed node kind " & $body[callPos].kind, body[callPos]) + +macro deed*(node): untyped = + result = impl(node) diff --git a/tests/config.nims b/tests/config.nims new file mode 100644 index 0000000..3bb69f8 --- /dev/null +++ b/tests/config.nims @@ -0,0 +1 @@ +switch("path", "$projectDir/../src") \ No newline at end of file diff --git a/tests/test1.nim b/tests/test1.nim new file mode 100644 index 0000000..5f45462 --- /dev/null +++ b/tests/test1.nim @@ -0,0 +1,50 @@ +when (compiles do: import nimbleutils/bridge): + import nimbleutils/bridge +else: + import unittest +import sequtils, algorithm + +import dirtydeeds + +test "basic cases": + check @[1, 2, 3].map(deed _ * 7) == @[7, 14, 21] + check @["A", "B", "C"].map(deed "foo" & (_: string)) == @["fooA", "fooB", "fooC"] + check @['a', 'f', 'A', '0', 'c'].filter(deed contains({'a'..'z'}, _)) == @['a', 'f', 'c'] + let a = deed (_: int) + (_: int) + check a(3, 4) == 7 + proc foo[T](a, b: T, x: proc (a, b: T): T): T = x(a, b) + proc foo[T](a: T, x: proc (a: T): T): T = x(a) + check foo(3, 4, deed _ + _) == 7 + let max0 = deed max(0, _: int) + let max0left = deed max(_: int, 0) + check max0(7) == 7 + check max0(-7) == 0 + check max0left(7) == 7 + check max0left(-7) == 0 + check foo(7, deed max(0, _)) == 7 + check foo(-7, deed max(0, _)) == 0 + check foo(7, deed max(_, 0)) == 7 + check foo(-7, deed max(_, 0)) == 0 + let b = deed (_(a): int) + a + check b(12) == 24 + check foo(7, deed _(a) + a * 2) == 21 + var s = @[5, 3, 4, 1, 9, 2] + s.sort(deed _ - _) + check s == @[1, 2, 3, 4, 5, 9] + s.sort(deed (_ a; _ b; b - a)) + check s == @[9, 5, 4, 3, 2, 1] + let maxDefault0 = deed max(_: int, (_: int) = 0) + check maxDefault0(7) == 7 + check maxDefault0(-7) == 0 + check maxDefault0(1, 7) == 7 + check maxDefault0(-1, -7) == -1 + +test "declaration": + proc foo {.deed.} = (_: string) & (_: char | string) + check foo("abc", 'd') == "abcd" + check foo("abc", "def") == "abcdef" + proc bar {.deed.} = max[_ T](_: T, _: T) + check bar(1, 2) == 2 + check bar(-1, -2) == -1 + # only works after 2.0 + #template baz {.deed.} = toSeq _