<pre><codeclass="remark_code">(vau dynamicEnv (normalParam1 normalParam2) (body of combiner))
</code></pre>
--
Lisps, as well as Kraken, have an _eval_ function.
This function takes in code as a data structure, and in R5RS Scheme an "environment specifier", and in Kraken, a full environment (like what is passed as _dynamicEnv_).
<pre><codeclass="remark_code">> (foldl or false (array true false))
true
</code></pre>
---
# Background: Fexprs - detail
All special forms in Kaken are combiners too, and are thus also first class.
In this case, we can not only pass the raw _if_ around, but we can make an _inverse_if_ which inverts its condition (kinda macro-like) and pass it around.
What were special forms in Lisp are now just built-in combiners in Kraken.
*if* is not any more special than *+*, and in both cases you can define your own versions that would be indistinguishable, and in both cases they are first-class.
5. Additionally, because it is unclear what code will be evaluated as a parameter to a function call and what code must be passed unevaluated to the combiner, little optimization can be done.
---
# Solution: Partial Eval
1. Partially evaluate a purely functional version of this language in a nearly-single pass over the entire program
2. Environment chains consisting of both "real" environments with every contained symbol mapped to a value and "fake" environments that only have placeholder values.
3. Since the language is purely functional, we know that if a symbol evaluates to a value anywhere, it will always evaluate to that value at runtime, and we can perform inlining and continue partial evaluation.
4. If the resulting partially-evaluated program only contains static references to a subset of built in combiners and functions (combiners that evaluate their parameters exactly once), the program can be compiled just like it was a normal Scheme program
It thus makes sense to ask if we can identify and partial evaluate / inline operative combiners to remove and optimize them like macros. Indeed, if we can determine what calls are to applicative combiners we can optimize their parameters, and if we can determine what calls are to macro-like operative combiners, we can try to do the equivalent of macro expansion.
- Online partial evaluation research generally does not have to deal with the same level of partially/fully dynamic and sometimes explicit environments
- \\(\kprim{0}{eval}\\): evaluates its argument in the given environment.
- \\(\kprim{0}{vau}\\): creates a new combiner and is analogous to lambda in other languages, but with a "wrap level" of 0, meaning the created combiner does not evaluate its arguments.
- \\(\kprim{0}{wrap}\\): increments the wrap level of its argument. Specifically, we are "wrapping" a "wrap level" n combiner (possibly "wrap level" 0, created by *vau* to create a "wrap level" n+1 combiner. A wrap level 1 combiner is analogous to regular functions in other languages.
- \\(\kprim{0}{unwrap}\\): decrements the "wrap level" of the passed combiner, the inverse of *wrap*.
- \\(\kprim{0}{if}\\): evaluates only its condition and converts to the \\(\kprim{0}{vif}\\) primitive for the next step. It cannot evaluate both branches due to the risk of non-termination.
- \\(\kprim{0}{vif}\\): evaluates and returns one of the two branches based on if the condition is non-zero.
- \\(\kprim{0}{int-to-symbol}\\): creates a symbol out of an integer.
- \\(\kprim{0}{array}\\): returns an array made out of its parameter list.
]
---
# Less Interesting Prims
.mathSize9[
- \\(\kcombine{\kprim{0}{type-test?}}{(A)}{E}\\): *array?*, *comb?*, *int?*, and *symbol?*, each return 0 if the single argument is of that type, otherwise they return 1.
- \\(\kcombine{\kprim{0}{len}}{(A)}{E}\\): returns the length of the single array argument.
- \\(\kcombine{\kprim{0}{idx}}{(A~n)}{E}\\): returns the nth item array A.
- \\(\kcombine{\kprim{0}{concat}}{(A~B)}{E}\\): combines both array arguments into a single concatenated array.
- \\(\kcombine{\kprim{0}{+}}{(A~A)}{E}\\): adds its arguments
- \\(\kcombine{\kprim{0}{<=}}{(A~A)}{E}\\): returns 0 if its arguments are in increasing order, and 1 otherwise.
]
---
# Base Language Summary
- This base calculus defined above is not only capable of normal lambda-calculus computations with primitives and derived user applicatives, but also supports a superset of macro-like behaviors via its support for operatives.
- All of the advantages listed in the introduction apply to this calculus, as do the performance drawbacks, at least if implemented naively. Our partial evaluation and compilation framework will demonstrate how to compile this base language into reasonably performant binaries (WebAssembly bytecode, for our prototype).
---
class: center, middle, title
# Slow
--
*so slow......*
---
# Partial Eval: How it works
- Online, no binding time analysis
- Partially Evaluate combiners with partially-static environments
- Prevent infinate recursion by blocking on
- Recursive calls underneath a partially evaluated body
- Recursive path to *if*
- Track call frames that need to be real to progress on every AST node
- Can zero-in on areas that will make progress
- Also tracks nodes previously stopped by recursion-stopper in case no longer under the frame that stopped the recursion
- Evaluate derived calls with parameter values, inline result even if not value if it doesn't depend on call frame
- When compiling in the wraplevel=1 side of conditional, further partial evaluate the parameter value
- Only a single branch of overhead for dynamic function calls
---
# Lazy Environment Instantiation
<pre><codeclass="remark_code">(lambda (f) (f))
</code></pre>
<pre><codeclass="remark_code">function(f):
if uses_env(f):
env_cache = make_env()
f(env_cache)
else:
f()
</code></pre>
Not only do we ask if *f* evaluate its parameters, but also does it take in an environment containing `{ f: <>, ...}`, etc
---
# Type-Inference-Based Primitive Inlining
For instance, consider the following code:
<pre><codeclass="remark_code">(cond (and (array? a) (= 3 (len a))) (idx a 2)
true nil)
</code></pre>
- Call to *idx* fully inlined without type or bounds checking
- No type information is needed to inline type predicates, as they only need to look at the tag bits.
- Equality checks can be inlined as a simple word/ptr compare if any of its parameters are of a type that can be word/ptr compared (ints, bools, and symbols).
---
# Immediately-Called Closure Inlining
Inlining calls to closure values that are allocated and then immediately used:
This is inlined
<pre><codeclass="remark_code">(let (a (+ 1 2))
(+ a 3))
</code></pre>
to this
<pre><codeclass="remark_code">((wrap (vau (a) (+ a 3))) (+ 1 2))
</code></pre>
and then inlined (plus lazy environment allocation)
---
# Y-Combinator Elimination
- When compiling a combiner, pre-emptive memoization
- Partial-evaluation to normalize
- Eager lang - extra lambda - eta-conversion in the compiler
---
# Current Status
1. No longer *super* slow
2. Fixed most BigO algo problems (any naive traversal is exponential)
3. Otherwise, the implementation is slow (pure function, Chicken Scheme not built for it, mostly un-profiled and optimized, etc)
4. Compiles wraplevel=0 combs to a assert(false), but simple to implement.
5. Working through bugs - right now figuring out why some things don't partially evaluate as far as they should
---
# Benchmarks
- Fib - Calculating the nth Fibonacci number
- RB-Tree - Inserting n items into a red-black tree, then traversing the tree to sum its values
- Deriv - Computing a symbolic derivative of a large expression
- Cfold - Constant-folding a large expression
- NQueens - Placing n number of queens on the board such that no two queens are diagonal, vertical, or horizontal from each other
---
# Results:
Number of eval calls with no partial evaluation for Fexprs