Skip to content

Commit

Permalink
direct symbol applicate, typed iterator test
Browse files Browse the repository at this point in the history
  • Loading branch information
metagn committed Jan 31, 2021
1 parent 1b414db commit 002dd9c
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 92 deletions.
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
# applicates

"pointers" to cached AST that instantiate routines when called. caches nodes of anonymous routine definitions then gives their pointer (index in the cache) which you can pass around as a compile time argument and invoke. not a clean solution but should do the job
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.

Would have preferred not using a cache to do this, but for now it should do the job.

```nim
import applicates
proc map[T](s: seq[T], f: ApplicateArg): seq[T] =
result.newSeq(s.len)
for i in 0..<s.len:
result[i] =
f.apply(s[i]) # maybe a little long
# or
f | s[i]
s[i] |< f # noisy, also you need to do tuples like ((1, 2)) |< f as (1, 2) |< f becomes f.apply(1, 2)
# or
f(s[i]) # uses experimental callOperator feature, if it breaks your code use `import except`
let x = s[i]
result[i] = f.apply(x)
# supported sugar for the above (best I could come up with, might be too much):
result[i] = f | x
result[i] = x |< f
result[i] = \f(x)
result[i] = \x.f
result[i] = f(x) # when experimental callOperator is enabled
result[i] = x.f # ditto
# `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]
# alternate syntax (doesnt look great but i cant think of anything better):
doAssert @[1, 2, 3, 4, 5].map(fromSymbol(succ)) == @[2, 3, 4, 5, 6]
# sugar for `applicate do` syntax (again, best I could come up with):
doAssert @[1, 2, 3, 4, 5].map(x !=> x * 2) == @[2, 4, 6, 8, 10]
doAssert @[1, 2, 3, 4, 5].map(x \=> x * 2) == @[2, 4, 6, 8, 10]
```

tests show some of the possibilities with this construct
See tests for more example uses of this library.

1 limitation is you can't really annotate these with types. you might think of a way with concepts but it's probably going to be way too complex to work
Note: Since `Applicate` is implemented as `distinct int` or `distinct string` and is also usually used as `static Applicate` (for which `ApplicateArg` is an alias), you might have a fair bit of trouble/bugs with the type system. This is unfortunate as it limits the possibilities for type annotated functional programming using applicates. The messiness of Nim's error system when dealing with macros also does not help in this regard.
5 changes: 2 additions & 3 deletions applicates.nimble
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# Package

version = "0.1.1"
version = "0.2.0"
author = "hlaaftana"
description = "\"pointers\" to cached AST that instantiate routines when called"
description = "instantiated \"pointers\" to cached AST"
license = "MIT"
srcDir = "src"


# Dependencies

requires "nim >= 0.20.0"
Expand Down
211 changes: 136 additions & 75 deletions src/applicates.nim
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ else:
type ApplicateArg* = static Applicate
## `static Applicate` to use for types of arguments

when cacheUseTable:
import macrocache
const applicateRoutineCache* = CacheTable "applicates.routines.table"
elif useCache:
when useCache:
import macrocache
const applicateRoutineCache* = CacheSeq "applicates.routines"
const applicateCache* =
when cacheUseTable:
CacheTable "applicates.applicates.table"
else:
CacheSeq "applicates.applicates"
elif cacheUseTable:
import tables
var applicateCache* {.compileTime.}: Table[string, NimNode]
else:
var applicateRoutineCache* {.compileTime.}: seq[NimNode]
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
Expand All @@ -46,8 +50,9 @@ macro makeApplicate*(body): untyped =
result = newNimNode(nnkStmtList, body)
for st in body: result.add(getAst(makeApplicate(st)))
of RoutineNodes:
let num = len(applicateRoutineCache)
let num = len(applicateCache)
let key = when cacheUseTable: $num else: num
# ^ is this even going to work in the future
result = newCall(bindSym"Applicate", newLit(key))
if body[0].kind != nnkEmpty:
result = newConstStmt(
Expand All @@ -73,9 +78,9 @@ macro makeApplicate*(body): untyped =
of nnkFuncDef: nskFunc
else: nskTemplate, "appl" & $num)
when cacheUseTable:
applicateRoutineCache[key] = b
applicateCache[key] = b
else:
add(applicateRoutineCache, b)
add(applicateCache, b)
else:
error("cannot turn non-routine into applicate, given kind " & $body.kind, body)

Expand Down Expand Up @@ -298,6 +303,21 @@ template `\=>`*(body): untyped =
## alias for `!=>`
applicate(body)

macro fromSymbol*(sym: untyped): Applicate =
## directly registers `sym` as an applicate node. might be more efficient
## than `toUntyped` for most cases, and hopefully shouldn't have to care
## about arity
runnableExamples:
const plus = fromSymbol(`+`)
doAssert plus.apply(1, 2) == 3
let num = len(applicateCache)
let key = when cacheUseTable: $num else: num
when cacheUseTable:
applicateCache[key] = sym
else:
add(applicateCache, sym)
result = newCall(bindSym"Applicate", newLit(key))

macro toUntyped*(sym: untyped, arity: static int): Applicate =
## creates an applicate with `n` = `arity` untyped parameters
## that calls the given symbol `sym`
Expand All @@ -312,68 +332,82 @@ 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)`
proc inferArity*(sym: NimNode): int =
## infers arity of symbol
##
## 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)
var s: string
s.setLen(4)
doAssert newstr.apply(4) == s

const leq = toUntyped(`<=`)
doAssert leq.apply(1, 2)
doAssert leq.apply(2.0, 2.0)
## -1 if sym is not a symbol, -2 if implementation
## of a symbol was nil, -3 if symbol choice arities
## do not match
case sym.kind
of nnkSym:
let impl = sym.getImpl
if impl.isNil:
error("implementation of symbol " & sym.repr & " for toUntyped was nil", sym)
if impl.isNil: return -2 # symbol impl nil
let fparams = impl[3]
var arity = 0
for i in 1..<fparams.len:
arity += fparams[i].len - 2
let identSym = ident repr sym
result = getAst(toUntyped(identSym, arity))
result += fparams[i].len - 2
of nnkClosedSymChoice, nnkOpenSymChoice:
var commonArity = 0
for i in 0..<sym.len:
let s = sym[i]
let impl = s.getImpl
if impl.isNil:
# maybe ignore these?
error("implementation of symbol choice " & s.repr & " for toUntyped was nil", sym)
if impl.isNil: return -2 # symbol impl nil
let fparams = impl[3]
var arity = 0
for i in 1..<fparams.len:
arity += fparams[i].len - 2
if i == 0:
commonArity = arity
elif commonArity != arity:
error("conflicting arities for symbol " & sym.repr & ": " &
$commonArity & " and " & $arity, s)
let identSym = ident repr sym
result = getAst(toUntyped(identSym, commonArity))
result = arity
elif result != arity:
result = -3 # symbol arities not shared
else:
result = -1

macro toUntyped*(sym: typed): Applicate =
## infers the arity of `sym` from its symbol then calls `toUntyped(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)
var s: string
s.setLen(4)
doAssert newstr.apply(4) == s

const leq = toUntyped(`<=`)
doAssert leq.apply(1, 2)
doAssert leq.apply(2.0, 2.0)
let arity = inferArity(sym)
case arity
of -1:
error("could not infer arity for non-symbol node " & sym.repr, sym)
of -2:
error("could not infer arity for symbol " & sym.repr & " with nil implementation", sym)
of -3:
error("arities not shared for choices for symbol " & sym.repr, sym)
else:
error("non-symbol was passed to unary toUntyped, with kind " & $sym.kind, sym)
let identSym = ident repr sym
result = getAst(toUntyped(identSym, arity))

proc node*(appl: Applicate): NimNode {.compileTime.} =
## retrieves the node of the applicate from the cache
applicateRoutineCache[(when cacheUseTable: string else: int)(appl)]
applicateCache[(when cacheUseTable: string else: int)(appl)]

macro arity*(appl: ApplicateArg): static int =
## gets arity of applicate
## gets arity of applicate. check `inferArity` for meaning of
## negative values
runnableExamples:
doAssert arity((x, y) !=> x + y) == 2
doAssert arity(a !=> a) == 1
doAssert arity(!=> 3) == 0
let fparams = appl.node[3]
var res = 0
for i in 1..<fparams.len:
res += fparams[i].len - 2
result = newLit(res)
let n = appl.node
case n.kind
of RoutineNodes:
let fparams = n[3]
var res = 0
for i in 1..<fparams.len:
res += fparams[i].len - 2
result = newLit(res)
else:
result = newLit(inferArity(n))

macro instantiateAs*(appl: ApplicateArg, name: untyped): untyped =
## instantiates the applicate in the scope with the given name
Expand All @@ -390,12 +424,29 @@ macro instantiateAs*(appl: ApplicateArg, name: untyped): untyped =
doAssert baz(1.0, 2.0) == 3.0
doAssert baz[uint8](1, 2) == 3u8
doAssert baz("a", "b") == "ab"
result = copy appl.node
result[0] =
if name.kind == nnkPrefix and name[0].eqIdent"*":
postfix(name[1], "*")
else:
name

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

let n = appl.node
case n.kind
of RoutineNodes:
result = copy n
result[0] =
if name.kind == nnkPrefix and name[0].eqIdent"*":
postfix(name[1], "*")
else:
name
else:
let argsSym = genSym(nskParam, "args")
result = newProc(
name = name,
params = [ident"untyped",
newIdentDefs(argsSym, newTree(nnkBracketExpr, ident"varargs", ident"untyped"))],
body = newCall(n, argsSym),
procType = nnkTemplateDef)

macro apply*(appl: ApplicateArg, args: varargs[untyped]): untyped =
## applies the applicate by injecting the applicate routine
Expand All @@ -404,19 +455,24 @@ macro apply*(appl: ApplicateArg, args: varargs[untyped]): untyped =
const incr = x !=> x + 1
doAssert incr.apply(1) == 2
let a = appl.node
let templName =
if a[0].kind in {nnkSym, nnkClosedSymChoice, nnkOpenSymChoice}:
ident repr a[0]
else:
a[0]
let aCall = newNimNode(nnkCall, args)
aCall.add(templName)
for arg in args:
aCall.add(arg)
result = quote do:
when not declared(`templName`):
`a`
`aCall`
case a.kind
of RoutineNodes:
let templName =
if a[0].kind in {nnkSym, nnkClosedSymChoice, nnkOpenSymChoice}:
ident repr a[0]
else:
a[0]
let aCall = newNimNode(nnkCall, args)
aCall.add(templName)
for arg in args:
aCall.add(arg)
result = quote do:
when not declared(`templName`):
`a`
`aCall`
else:
result = newCall(a)
for arg in args: result.add(arg)

macro forceApply*(appl: ApplicateArg, args: varargs[untyped]): untyped =
## applies the applicate by injecting the applicate routine,
Expand All @@ -425,16 +481,21 @@ macro forceApply*(appl: ApplicateArg, args: varargs[untyped]): untyped =
## realistically, the applicate routine is never in scope, but if you
## really come across a case where it is then you can use this
let a = appl.node
let templName =
if a[0].kind in {nnkSym, nnkClosedSymChoice, nnkOpenSymChoice}:
ident repr a[0]
else:
a[0]
let aCall = newNimNode(nnkCall, args)
aCall.add(templName)
for arg in args:
aCall.add(arg)
result = newBlockStmt(newStmtList(a, aCall))
case a.kind
of RoutineNodes:
let templName =
if a[0].kind in {nnkSym, nnkClosedSymChoice, nnkOpenSymChoice}:
ident repr a[0]
else:
a[0]
let aCall = newNimNode(nnkCall, args)
aCall.add(templName)
for arg in args:
aCall.add(arg)
result = newBlockStmt(newStmtList(a, aCall))
else:
result = newCall(a)
for arg in args: result.add(arg)

template `()`*(appl: ApplicateArg, args: varargs[untyped]): untyped =
## Call operator alias for `apply`. Must turn on experimental Nim feature
Expand Down
6 changes: 6 additions & 0 deletions tests/test_basic.nim
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,9 @@ test "toUntyped":
const adder = toUntyped(`+`, 2)
const toString = toUntyped(`$`)
check (2, 3) |< adder |< toString == "5"

test "fromSymbol":
const adder = fromSymbol(`+`)
const toString = fromSymbol(`$`)
const next = fromSymbol(succ)
check (2, 3) |< adder |< next |< toString == "6"
4 changes: 2 additions & 2 deletions tests/test_call_operator.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import unittest, applicates

test "call operator works":
applicate double(x): x * 2
applicate double do (x): x * 2
check double(3) == 6
check 3.double == 6

test "infix call":
applicate `++`(x, y): x + y
applicate `++` do (x, y): x + y

check 1 ++ 2 == 3
2 changes: 1 addition & 1 deletion tests/test_derive.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ template mix(h: var Hash, i) = h = h !& i
template finish(h: var Hash) = h = !$ h

template class(name, decls) =
applicate `name`(Self):
applicate `name` do (Self {.inject.}):
decls

class Hashable:
Expand Down
Loading

0 comments on commit 002dd9c

Please sign in to comment.