mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-30 18:02:05 +00:00
This plugin provides essential building block for implementing incremental computations in your programs. The idea behind incremental computations is that if you do the same calculation multiple times but with slightly different inputs you don't have to recompute everything from scratch. Also you don't want to adopt special algorithms either, you would like to write your code in standard from scratch manner and get incrementality for free when it is possible. The plugin computes the digest of the proc bodies, recursively hashing all called procs as well . Such digest with the digest of the argument values gives a good "name" for the result. Terminology loosely follows paper "Incremental Computation with Names" link below. It works well if you have no side effects in your computations. If you have global state in your computations then you will need problem specific workarounds to represent global state in set of "names" . SideEffect tracking in Nim also useful in this topic. Classical examples: Dashboard with ticking data. New data arrives non stop and you would like to update the dashboard recomputing only changed outputs. Excel spreadsheet where user changes one cell and you would like to recompute all cells that are affected by the change, but do not want to recompute every cell in the spreadsheet.
151 lines
4.6 KiB
Nim
151 lines
4.6 KiB
Nim
discard """
|
|
output: '''heavy_calc_impl is called
|
|
sub_calc1_impl is called
|
|
sub_calc2_impl is called
|
|
** no changes recompute effectively
|
|
** change one input and recompute effectively
|
|
heavy_calc_impl is called
|
|
sub_calc2_impl is called'''
|
|
"""
|
|
|
|
# sample incremental
|
|
|
|
import tables
|
|
import macros
|
|
|
|
var inputs = initTable[string, float]()
|
|
var cache = initTable[string, float]()
|
|
var dep_tree {.compileTime.} = initTable[string, string]()
|
|
|
|
macro symHash(s: typed{nkSym}): string =
|
|
result = newStrLitNode(symBodyHash(s))
|
|
|
|
#######################################################################################
|
|
|
|
template graph_node(key: string) {.pragma.}
|
|
|
|
proc tag(n: NimNode): NimNode =
|
|
## returns graph node unique name of a function or nil if it is not a graph node
|
|
expectKind(n, {nnkProcDef, nnkFuncDef})
|
|
for p in n.pragma:
|
|
if p.len > 0 and p[0] == bindSym"graph_node":
|
|
return p[1]
|
|
return nil
|
|
|
|
macro graph_node_key(n: typed{nkSym}): untyped =
|
|
result = newStrLitNode(n.symBodyHash)
|
|
|
|
macro graph_discovery(n: typed{nkSym}): untyped =
|
|
# discovers graph dependency tree and updated dep_tree global var
|
|
let mytag = newStrLitNode(n.symBodyHash)
|
|
var visited: seq[NimNode]
|
|
proc discover(n: NimNode) =
|
|
case n.kind:
|
|
of nnkNone..pred(nnkSym), succ(nnkSym)..nnkNilLit: discard
|
|
of nnkSym:
|
|
if n.symKind in {nskFunc, nskProc}:
|
|
if n notin visited:
|
|
visited.add n
|
|
let tag = n.getImpl.tag
|
|
if tag != nil:
|
|
dep_tree[tag.strVal] = mytag.strVal
|
|
else:
|
|
discover(n.getImpl.body)
|
|
else:
|
|
for child in n:
|
|
discover(child)
|
|
discover(n.getImpl.body)
|
|
result = newEmptyNode()
|
|
|
|
#######################################################################################
|
|
|
|
macro incremental_input(key: static[string], n: untyped{nkFuncDef}): untyped =
|
|
# mark leaf nodes of the graph
|
|
template getInput(key) {.dirty.} =
|
|
{.noSideEffect.}:
|
|
inputs[key]
|
|
result = n
|
|
result.pragma = nnkPragma.newTree(nnkCall.newTree(bindSym"graph_node", newStrLitNode(key)))
|
|
result.body = getAst(getInput(key))
|
|
|
|
macro incremental(n: untyped{nkFuncDef}): untyped =
|
|
## incrementalize side effect free computation
|
|
## wraps function into caching layer, mark caching function as a graph_node
|
|
## injects dependency discovery between graph nodes
|
|
template cache_func_body(func_name, func_name_str, func_call) {.dirty.} =
|
|
{.noSideEffect.}:
|
|
graph_discovery(func_name)
|
|
let key = graph_node_key(func_name)
|
|
if key in cache:
|
|
result = cache[key]
|
|
else:
|
|
echo func_name_str & " is called"
|
|
result = func_call
|
|
cache[key] = result
|
|
|
|
let func_name = n.name.strVal & "_impl"
|
|
let func_call = nnkCall.newTree(ident func_name)
|
|
for i in 1..<n.params.len:
|
|
func_call.add n.params[i][0]
|
|
let cache_func = n.copyNimTree
|
|
cache_func.body = getAst(cache_func_body(ident func_name, func_name, func_call))
|
|
cache_func.pragma = nnkPragma.newTree(newCall(bindSym"graph_node",
|
|
newCall(bindSym"symHash", ident func_name)))
|
|
|
|
n.name = ident(func_name)
|
|
result = nnkStmtList.newTree(n, cache_func)
|
|
|
|
###########################################################################
|
|
### Example
|
|
###########################################################################
|
|
|
|
func input1(): float {.incremental_input("a1").}
|
|
|
|
func input2(): float {.incremental_input("a2").}
|
|
|
|
func sub_calc1(a: float): float {.incremental.} =
|
|
a + input1()
|
|
|
|
func sub_calc2(b: float): float {.incremental.} =
|
|
b + input2()
|
|
|
|
func heavy_calc(a: float, b: float): float {.incremental.} =
|
|
sub_calc1(a) + sub_calc2(b)
|
|
|
|
###########################################################################
|
|
## graph finalize and inputs
|
|
###########################################################################
|
|
|
|
macro finalize_dep_tree(): untyped =
|
|
result = nnkTableConstr.newNimNode
|
|
for key, val in dep_tree:
|
|
result.add nnkExprColonExpr.newTree(newStrLitNode key, newStrLitNode val)
|
|
result = nnkCall.newTree(bindSym"toTable", result)
|
|
|
|
const dep_tree_final = finalize_dep_tree()
|
|
|
|
proc set_input(key: string, val: float) =
|
|
## set input value
|
|
## all affected nodes of graph are invalidated
|
|
inputs[key] = val
|
|
var k = key
|
|
while k != "":
|
|
k = dep_tree_final.getOrDefault(k , "")
|
|
cache.del(k)
|
|
|
|
###########################################################################
|
|
## demo
|
|
###########################################################################
|
|
|
|
set_input("a1", 5)
|
|
set_input("a2", 2)
|
|
discard heavy_calc(5.0, 10.0)
|
|
|
|
echo "** no changes recompute effectively"
|
|
discard heavy_calc(5.0, 10.0)
|
|
|
|
echo "** change one input and recompute effectively"
|
|
|
|
set_input("a2", 10)
|
|
discard heavy_calc(5.0, 10.0)
|