Skip to content

Commit

Permalink
large refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
metagn committed Sep 17, 2022
1 parent ea9ba42 commit 717f927
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 256 deletions.
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
# applicates

instantiated "pointers" to cached AST. caches nodes of anonymous routine definitions OR symbols then returns their pointer (index/key in the cache) which you can pass around as a compile time argument and instantiate in order to use. this allows for fully inlined lambdas via *"anonymous templates"*, which is the construct that the macros in this library mainly focus on.
Generalized routine and symbol pointers, achieved by instantiating cached
routine definitions or symbols. The cached AST is referenced by a key,
this key is passed around as a compile time value to be instantiated.

Would have preferred not using a cache to do this, but for now it should do the job.
This allows for fully inlined lambdas via anonymous templates, which is
the construct that the macros in this library mainly focus on.

```nim
import applicates
# optional operators:
import applicates/operators
# ApplicateArg is static Applicate
proc map[T](s: seq[T], f: ApplicateArg): seq[T] =
result.newSeq(s.len)
for i in 0..<s.len:
let x = s[i]
result[i] = f.apply(x)
# optional operators:
result[i] = x |> f # injects x into right hand side
# with `import applicates/calloperator`:
result[i] = f(x)
result[i] = x.f
# with `import applicates/operators`:
result[i] = \f(x)
result[i] = \x.f
result[i] = f(x) # when experimental callOperator is enabled
result[i] = x.f # ditto
result[i] = x |> f
# `applicate do` here generates an anonymous template, so `x - 1` is inlined at AST level:
doAssert @[1, 2, 3, 4, 5].map(applicate do (x): x - 1) == @[0, 1, 2, 3, 4]
doAssert @[1, 2, 3, 4, 5].map(fromSymbol(succ)) == @[2, 3, 4, 5, 6]
# optional operators:
doAssert @[1, 2, 3, 4, 5].map(applicate do (x: int) -> int: x - 1) == @[0, 1, 2, 3, 4]
doAssert @[1, 2, 3, 4, 5].map(toApplicate(succ)) == @[2, 3, 4, 5, 6]
doAssert @[1, 2, 3, 4, 5].map(x ==> x * 2) == @[2, 4, 6, 8, 10]
```

See tests for more example uses of this library. Tests are ran for multiple backends.

Note: Since `Applicate` is implemented as `distinct ApplicateKey` and is also usually used as `static Applicate` (for which `ApplicateArg` is an alias), this library fairly pushes Nim's type system, so annotating applicates with types can be difficult. Nim macro errors in general are also not great.
Note: Since `Applicate` is implemented as `distinct ApplicateKey` and is also usually used as `static Applicate` (for which `ApplicateArg` is an alias), this library fairly pushes Nim's type system, and errors are likely to be cryptic.
8 changes: 3 additions & 5 deletions applicates.nimble
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Package

version = "0.3.1"
version = "0.4.0"
author = "metagn"
description = "instantiated \"pointers\" to cached AST"
description = "generalized routine and symbol pointers"
license = "MIT"
srcDir = "src"

Expand All @@ -27,9 +27,7 @@ task tests, "run tests for multiple backends and defines":
backends = {c, nims},
optionCombos = @[
"",
"-d:applicatesUseMacroCache",
"-d:applicatesCacheUseTable",
"-d:applicatesUseMacroCache -d:applicatesCacheUseTable"]
"-d:applicatesCacheUseTable"]
)
else:
echo "tests task not implemented, need nimbleutils"
82 changes: 68 additions & 14 deletions src/applicates.nim
Original file line number Diff line number Diff line change
Expand Up @@ -248,20 +248,39 @@ macro applicate*(body): untyped =
let args = newPar()
result = getAst(applicate(args, body))

macro fromSymbol*(sym: untyped): Applicate =
template `==>`*(params, body): untyped =
## infix version of `applicate`, same parameter syntax
runnableExamples:
doAssert (x ==> x + 1).apply(2) == 3
const foo = (a, b) ==> a + b
doAssert foo.apply(1, 2) == 3
applicate(params, body)

template `==>`*(body): untyped =
## same as ``applicate(body)``
applicate(body)

macro toApplicate*(sym: untyped): Applicate =
## directly registers `sym` as an applicate node. might be more efficient
## than `toUntyped` for most cases, and accepts varying arities
## than `toCallerApplicate` for most cases, and accepts varying arities
runnableExamples:
const plus = fromSymbol(`+`)
const plus = toApplicate(`+`)
doAssert plus.apply(1, 2) == 3
let key = registerApplicate(sym)
result = newCall(bindSym"Applicate", newLit(key))

macro toUntyped*(sym: untyped, arity: static int): Applicate =
## creates an applicate with `n` = `arity` untyped parameters
template `&&`*(sym): untyped =
## same as ``toApplicate(sym)``
runnableExamples:
const foo = &&min
doAssert foo.apply(1, 2) == 1
toApplicate(sym)

macro toCallerApplicate*(sym: untyped, arity: static int): Applicate =
## creates an applicate of a template with `n` = `arity` untyped parameters
## that calls the given symbol `sym`
runnableExamples:
const adder = toUntyped(`+`, 2)
const adder = toCallerApplicate(`+`, 2)
doAssert adder.apply(1, 2) == 3
var params = newNimNode(nnkPar)
var call = newCall(sym)
Expand All @@ -271,18 +290,18 @@ macro toUntyped*(sym: untyped, arity: static int): Applicate =
call.add(temp)
result = getAst(applicate(params, call))

macro toUntyped*(sym: typed): Applicate =
## infers the arity of `sym` from its symbol then calls `toUntyped(sym, arity)`
macro toCallerApplicate*(sym: typed): Applicate =
## infers the arity of `sym` from its symbol then calls `toCallerApplicate(sym, arity)`
##
## if `sym` is a symbol choice, then the common arity of the choices is used.
## if the symbol choices do not share an arity, it will give an error
runnableExamples:
const newstr = toUntyped(newString)
const newstr = toCallerApplicate(newString)
var s: string
s.setLen(4)
doAssert newstr.apply(4) == s

const leq = toUntyped(`<=`)
const leq = toCallerApplicate(`<=`)
doAssert leq.apply(1, 2)
doAssert leq.apply(2.0, 2.0)
let arity = inferArity(sym)
Expand All @@ -295,15 +314,15 @@ macro toUntyped*(sym: typed): Applicate =
error("arities not shared for choices for symbol " & sym.repr, sym)
else:
let identSym = ident repr sym
result = getAst(toUntyped(identSym, arity))
result = getAst(toCallerApplicate(identSym, arity))

macro instantiateAs*(appl: ApplicateArg, name: untyped): untyped =
## instantiates the applicate in the scope with the given name
##
## helps where `apply` syntax isn't enough (for example generics
## and overloading)
runnableExamples:
instantiateAs(fromSymbol(system.succ), incr)
instantiateAs(toApplicate(system.succ), incr)
doAssert incr(1) == 2
proc foo[T](x, y: T): T {.makeApplicate.} = x + y
proc bar(x, y: string): string {.makeApplicate.} = x & y
Expand All @@ -314,7 +333,7 @@ macro instantiateAs*(appl: ApplicateArg, name: untyped): untyped =
doAssert baz("a", "b") == "ab"

# also works but less efficient as new template is generated:
instantiateAs(fromSymbol(`-`), minus)
instantiateAs(toApplicate(`-`), minus)
doAssert minus(4) == -4
doAssert minus(5, 2) == 3

Expand All @@ -336,11 +355,46 @@ macro instantiateAs*(appl: ApplicateArg, name: untyped): untyped =
body = newCall(n, argsSym),
procType = nnkTemplateDef)

macro toSymbol*(appl: ApplicateArg): untyped =
## retrieves the symbol of the applicate,
## also instantiates routine definitions
runnableExamples:
template foo(x: int): int = x + 1
const incr = toApplicate(foo)
doAssert toSymbol(incr)(1) == 2
let a = appl.node
case a.kind
of RoutineNodes:
let templName =
if a[0].kind in {nnkSym, nnkClosedSymChoice, nnkOpenSymChoice}:
ident repr a[0]
else:
a[0]
let declaredCheck = prefix(newCall(bindSym"declared", templName), "not")
result = newStmtList(newTree(nnkWhenStmt, newTree(nnkElifBranch, declaredCheck, a)), templName)
else:
result = a

macro forceToSymbol*(appl: ApplicateArg): untyped =
## retrieves the symbol of the applicate, also
## instantiates routine definitions, without reusing definitions in scope
let a = appl.node
case a.kind
of RoutineNodes:
let templName =
if a[0].kind in {nnkSym, nnkClosedSymChoice, nnkOpenSymChoice}:
ident repr a[0]
else:
a[0]
result = newBlockStmt(newStmtList(a, templName))
else:
result = a

macro apply*(appl: ApplicateArg, args: varargs[untyped]): untyped =
## applies the applicate by injecting the applicate routine
## (if not in scope already) then calling it with the given arguments
runnableExamples:
const incr = fromSymbol(system.succ)
const incr = toApplicate(system.succ)
doAssert incr.apply(1) == 2
let a = appl.node
case a.kind
Expand Down
5 changes: 5 additions & 0 deletions src/applicates/calloperator.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ../applicates

template `()`*(appl: ApplicateArg, args: varargs[untyped]): untyped =
## Call operator alias for `apply`.
appl.apply(args)
34 changes: 12 additions & 22 deletions src/applicates/internals.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import macros
import macros, macrocache

const cacheUseTable = defined(applicatesCacheUseTable) and not defined(nimdoc)
const useCache = defined(applicatesUseMacroCache) and not defined(nimdoc)

when cacheUseTable:
type ApplicateKey* = string
Expand All @@ -16,26 +15,17 @@ type
ApplicateArg* = static Applicate
## `static Applicate` to use for types of arguments

when useCache:
import macrocache
const applicateCache* =
when cacheUseTable:
CacheTable "applicates.applicates.table"
else:
CacheSeq "applicates.applicates"
elif cacheUseTable:
import tables
var applicateCache* {.compileTime.}: Table[string, NimNode]
else:
var applicateCache* {.compileTime.}: seq[NimNode]
## the cache containing the routine definition nodes of
## each applicate. can be indexed by the ID of an applicate to receive
## its routine node, which is meant to be put in user code and invoked
##
## uses a compileTime seq by default, if you define `applicatesUseMacroCache`
## then it will use Nim's `macrocache` types, if you define
## `applicatesCacheUseTable` then it will use a `CacheTable` with
## unique strings
const applicateCache* =
when cacheUseTable:
CacheTable "applicates.applicates.table"
else:
CacheSeq "applicates.applicates"
## the cache containing the routine definition nodes of
## each applicate. can be indexed by the ID of an applicate to receive
## its routine node, which is meant to be put in user code and invoked
##
## if you define `applicatesCacheUseTable` then it will use
## a `CacheTable` with unique strings

template applicateCount*(): int = # this breaks when `proc {.compileTime.}`
## total number of registered applicates
Expand Down
51 changes: 12 additions & 39 deletions src/applicates/operators.nim
Original file line number Diff line number Diff line change
@@ -1,32 +1,5 @@
import ../applicates, macros

template `==>`*(params, body): untyped =
## infix version of `applicate`, same parameter syntax
runnableExamples:
import ../applicates
doAssert (x ==> x + 1).apply(2) == 3
const foo = (a, b) ==> a + b
doAssert foo.apply(1, 2) == 3
applicate(params, body)

template `==>`*(body): untyped =
## same as ``applicate(body)``
applicate(body)

template `()`*(appl: ApplicateArg, args: varargs[untyped]): untyped =
## Call operator alias for `apply`. Must turn on experimental Nim feature
## `callOperator` to use. Note that this experimental feature seems to be
## fairly broken. This definition might also go away if Nim starts to error
## on templates named to overload experimental operators (which it currently
## doesn't inconsistently with other routines), as a `compiles` check does
## not work with the `experimental` pragma in other modules.
##
## It's hard to conditionally define routines based on experimental features.
## Nim currently does not error with experimental operator overloads if they
## are templates, so this specific routine works. However if you run into
## problems with the call operator, `import except` should do the trick.
appl.apply(args)

proc insertApply*(call: NimNode): NimNode =
## turns a regular call node into an `apply` call
result = newNimNode(if call.kind == nnkCommand: nnkCommand else: nnkCall, call)
Expand Down Expand Up @@ -77,9 +50,9 @@ macro `|>`*(value, call): untyped =
runnableExamples:
import ../applicates

const incr = fromSymbol(system.succ)
const multiply = fromSymbol(`*`)
const divide = fromSymbol(`/`)
const incr = toApplicate(system.succ)
const multiply = toApplicate(`*`)
const divide = toApplicate(`/`)

let foo = 3 |>
multiply(2) |>
Expand All @@ -93,13 +66,13 @@ macro `\>`*(value, call): untyped =
runnableExamples:
import ../applicates

const incr = fromSymbol(system.succ)
const multiply = fromSymbol(`*`)
const divide = fromSymbol(`/`)
const incr = toApplicate(system.succ)
const multiply = toApplicate(`*`)
const divide = toApplicate(`/`)

let foo = 3 |>
multiply(2) |>
incr |>
let foo = (3, 2) \>
multiply \>
incr \>
14.divide
doAssert foo == 2
result = call
Expand All @@ -115,9 +88,9 @@ macro chain*(initial, calls): untyped =
runnableExamples:
import ../applicates

const incr = fromSymbol(system.succ)
const multiply = fromSymbol(`*`)
const divide = fromSymbol(`/`)
const incr = toApplicate(system.succ)
const multiply = toApplicate(`*`)
const divide = toApplicate(`/`)

let foo = chain 3:
multiply 2
Expand Down
14 changes: 7 additions & 7 deletions tests/test_basic.nim
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ test "map test":
s.add(y)
check s == @[1/7, 2/7, 3/7, 4/7, 5/7]

test "toUntyped":
const adder = toUntyped(`+`, 2)
const toString = toUntyped(`$`)
test "toCallerApplicate":
const adder = toCallerApplicate(`+`, 2)
const toString = toCallerApplicate(`$`)
check \adder(2, 3) |> toString == "5"

test "fromSymbol":
const adder = fromSymbol(`+`)
const toString = fromSymbol(`$`)
const next = fromSymbol(succ)
test "toApplicate":
const adder = toApplicate(`+`)
const toString = toApplicate(`$`)
const next = toApplicate(succ)
check ((2, 3) \> adder |> next) \> toString == "6"
6 changes: 1 addition & 5 deletions tests/test_call_operator.nim
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
{.experimental: "callOperator".}

when (compiles do: import nimbleutils/bridge):
import nimbleutils/bridge
else:
import unittest

import applicates

from applicates/operators import `()`
import applicates, applicates/calloperator

test "call operator works":
applicate double do (x): x * 2
Expand Down
Loading

0 comments on commit 717f927

Please sign in to comment.