Ok, Fexprs are calls to combiners - combiners are either applicatives or operatives.
Combiners are introduced with _vau_ and take an extra paramter (here called _dynamic_env_, earlier called _de_) which is the dynamic environment.
<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_).
- Functions - runtime, evaluate parameters once, return value
--
- Macros - expansion time, do not evaluate parameters, return code to be inlined
--
- Special Forms - look like function or macro calls, but do something special (if, lambda, etc)
--
- **Kraken** (and Kernel)
--
- Combiners
--
- Applicatives (like normal functions, combiners that evaluate all their parameters once in their dynamic environment)
--
- Operatives (combiners that do something unusual with their parameters, do not evaluate them right away)
--
_Operatives can replace macros and special forms, so combiners replace all_
---
# Background: Fexprs - detail
Combiners, like functions in Lisp, are first class.
This means that unlike in Lisp, Kraken's version of macros and special forms are *both* first class.
---
# Background: Fexprs - detail
As we've mentioned, in Scheme _or_ is a macro expanding
<pre><codeclass="remark_code">(or a b)
</code></pre>
to
<pre><codeclass="remark_code">(let ((temp a))
(if temp temp
b))
</code></pre>
So passing it to a higher-order function doesn't work, you have to wrap it in a function:
<pre><codeclass="remark_code">> (fold or #f (list #t #f))
Exception: invalid syntax and
> (fold (lambda (a b) (or a b)) #f (list #t #f))
#t
</code></pre>
---
# Background: Fexprs - detail
But in Kraken, _or_ is a combiner (an operative!), so it's first-class
<pre><codeclass="remark_code">(vau de (a b) (let ((temp (eval a de)))
(if temp temp
(eval b de))))
</code></pre>
So it's pefectly legal to pass to a higher-order combiner:
<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
Macros, espicially *define-macro* macros, are essentially functions that runat expansion time and compute new code from old code.
This is essentially partial evaluation / inlining, depending on how you look at it.
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 equlivant of macro expansion.
For Kraken, this is exactly what we do, using a specialized form of Partial Evaluation to do so.
---
# Challenges
So what's the hard part? Why hasn't this been done before?
- Detour through Lisp history?
Determining even what code will be evaluated is difficult.
- Partial Evaluation
- Can't use a binding time analysis pass with offline partial evaluation, which elminates quite a bit of mainline partial evaluation research
- Online partial evaluation research generally does not have to deal with the same level of partially/fully dynamic and sometimes explicit environments
- woo
---
# Research
- *Practical compilation of fexprs using partial evaluation*
- Currently under review for ICFP '23
- Wrote partial evaluator with compiler
- Heavily specialized to optimize away operative combiners like macros written in a specific way
- Prototype faster than Python and other interpreted Lisps
- Static calls fully optimized like a normal function call in other langauges
- Dynamic calls have a single branch of overhead - if normal applicative combiner function like call, post-branch optimized as usual
- Optimizes away Y Combinator recursion to static recursive jumps (inc tail call opt)
- Bit of an odd language: purely functional, array based, environment values
---
# Research
**insert benchmarks**
---
# Current Research
- More normal language: purely functional Scheme
- Environments are just association lists
- Fully manipulateable as normal list/pairs
- Partial evaluation that supports naturaly-written operative combiners, like the running *or* example
1. We will wrap angle brackets <> around values that are not representable in the syntax of the language - i.e. + is a symbol that will be looked up in an environment, <+> is the addition function.
2. We will use square brackets [] to indiciate array values, and we will use a single quote to indicate symbol values ', for instance '+ is the symbol + as a value.
3. Additionally, we will use curly braces ({}) to indicate the environment (mapping symbols to values). Elipses will be used to omit unnecessary information.
4. Finally, we will not show the static environment nested in combiners, but know that each combiner carries with it the environment it was created with, which becomes the upper environment when its body is executing (the immediate environment being populated with the parameters).
---
# A few more things..
1. ; is the comment character for the language
2. We will sometimes make larger evaluation jumps for (some) brevity
3. wraplevel is how many times a combiner will evaluate its parameters before the body starts executing. 0 makes it work like a macro, 1 is like a function, etc
4. Wrap takes a combiner and returns the same combiner with an incremented wraplevel, unwrap does the reverese
5. Typing these examples by hand is too finicky, next time they'll be autogenerated with color by the prototype partial evaluator!
---
{ ...root environment...}
(wrap (vau (n) (* n 2)))
---
# Small Vau-specific Example (implementing quote)
{ ...root environment...}
((vau (x) x) hello)
---
# Conclusion: slow
1. Look at all of the steps it took to simply get a function value that multiplies by 2!
2. This would make our program much slower if this happened at runtime, for every function in the program.
3. What's the solution? Partial Evaluation!
---
# Partial Eval: How it works
1. Evaluate as much as possible ahead of time, at compile time.
2. If some call sites are indeterminate, they can still be compiled, but there will have to be a runtime check inserted that splits evaluation based on if the combiner evaluates its parameters or not, and eval and all builtins will have to be compiled into the resulting executable.
3. When compiling in the wraplevel=1 side of conditional, further partial evaluate the parameter value
---
# Partial evaluation could have done all the work from that last big example at compile time, leaving only the final value to be compiled:
<combwraplevel=1(n)(*n2)>
Additionally, if this was a more complex function that used other functions, those functions would also generally be fully partially evaluated at compile time.
It's the full power of Vau/Combiners/Fexprs with the expected runtime performance of Scheme!
---
# Partial Eval: 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
---
# Partial Eval: Future
Allow type systems to be built using Vaus, like the type-systems-as-macros paper (https://www.ccs.neu.edu/home/stchang/pubs/ckg-popl2017.pdf).
This type system could pass down type hints to the partial evaluator, enabling:
2. Compiletime: Drop optimizing compiled version if wraplevel=0
3. Compiletime: Drop emitting constant code for if wraplevel=1
4. Runtime: Eliminate branch on wrap level
5. Runtime: Eliminate other typechecks for builtin functions