More work including brain dumping more slides and bugfixes for memo - have found current problem, which is that a non-env se can prevent a DeriComb from pulling in valeus from a real Env frame which it's being returned from - even though this is caught, the resulting call still depends on it, marks it as such, and then it depends on a call that was real and now no longer exists.
This commit is contained in:
@@ -149,16 +149,25 @@ impl NeededIds {
|
||||
if_stopped: self.if_stopped.union(&other.if_stopped).cloned().collect(),
|
||||
}
|
||||
}
|
||||
fn union_without(&self, other: &NeededIds, without: &EnvID) -> Self {
|
||||
//fn union_without(&self, other: &NeededIds, without: &EnvID) -> Self {
|
||||
//NeededIds {
|
||||
//heads: self.heads.union(&other.heads) .filter(|x| *x != without).cloned().collect(),
|
||||
//tails: self.tails.union(&other.tails) .filter(|x| *x != without).cloned().collect(),
|
||||
//body_stopped: self.body_stopped.union(&other.body_stopped).filter(|x| *x != without).cloned().collect(),
|
||||
//if_stopped: self.if_stopped.union(&other.if_stopped) .filter(|x| *x != without).cloned().collect(),
|
||||
//}
|
||||
//}
|
||||
fn without(&self, without: &EnvID) -> Self {
|
||||
NeededIds {
|
||||
heads: self.heads.union(&other.heads) .filter(|x| *x != without).cloned().collect(),
|
||||
tails: self.tails.union(&other.tails) .filter(|x| *x != without).cloned().collect(),
|
||||
body_stopped: self.body_stopped.union(&other.body_stopped).filter(|x| *x != without).cloned().collect(),
|
||||
if_stopped: self.if_stopped.union(&other.if_stopped) .filter(|x| *x != without).cloned().collect(),
|
||||
heads: self.heads.iter() .filter(|x| *x != without).cloned().collect(),
|
||||
tails: self.tails.iter() .filter(|x| *x != without).cloned().collect(),
|
||||
body_stopped: self.body_stopped.iter().filter(|x| *x != without).cloned().collect(),
|
||||
if_stopped: self.if_stopped.iter() .filter(|x| *x != without).cloned().collect(),
|
||||
}
|
||||
}
|
||||
fn union_into_tail(&self, other: &NeededIds, without_tail: Option<&EnvID>) -> Self {
|
||||
let tails: BTreeSet<EnvID> = self.tails.union(&other.heads).chain(other.tails.iter()).cloned().filter(|x| without_tail.is_none() || x != without_tail.unwrap()).collect();
|
||||
let new_tails = other.heads.union(&other.tails).filter(|x| without_tail.is_none() || *x != without_tail.unwrap());
|
||||
let tails: BTreeSet<EnvID> = self.tails.iter().chain(new_tails).cloned().collect();
|
||||
assert!(!tails.contains(&true_id));
|
||||
NeededIds {
|
||||
heads: self.heads.clone(),
|
||||
@@ -354,8 +363,19 @@ impl DCtx {
|
||||
real_set: Rc::clone(&self.real_set), fake_set: Rc::clone(&self.fake_set), fake_if_set: new_fake_if_set, ident: self.ident+1 })
|
||||
}
|
||||
|
||||
pub fn can_progress(&self, ids: NeededIds) -> bool {
|
||||
//pub fn can_progress(&self, ids: NeededIds) -> bool {
|
||||
pub fn can_progress(&self, x: &Rc<MarkedForm>) -> bool {
|
||||
let ids = x.ids();
|
||||
// check if ids is true || ids intersection EnvIDs in our stacks is non empty || ids.hashes - current is non empty
|
||||
let all_needed: BTreeSet<EnvID> = ids.heads.union(&ids.tails).filter(|x| **x != true_id).cloned().collect();
|
||||
let all_possible: BTreeSet<EnvID> = self.real_set.union(&self.fake_set).cloned().collect();
|
||||
let ok = all_possible.is_superset(&all_needed);
|
||||
if !ok {
|
||||
println!("Gah - needed {:?}", all_needed);
|
||||
println!("Gah - have total (fake and real) {:?}", all_possible);
|
||||
println!("it: {}", x);
|
||||
}
|
||||
assert!(ok);
|
||||
ids.heads.contains(&true_id) || !self.real_set.is_disjoint(&ids.heads) || !self.fake_set.is_superset(&ids.body_stopped) || !self.fake_if_set.is_superset(&ids.if_stopped)
|
||||
}
|
||||
}
|
||||
@@ -474,7 +494,8 @@ impl MarkedForm {
|
||||
// do we ever need body ids except for true?
|
||||
// and can we remove se at some point?
|
||||
//let ids = if !se.is_legal_env_chain().unwrap() { se.ids() } else { se.ids().union_without(&body.ids(), &id) };
|
||||
let ids = if body.ids().may_contain_id(&true_id) { se.ids().union(&NeededIds::new_true()) } else { se.ids() };
|
||||
//let ids = if body.ids().may_contain_id(&true_id) { se.ids().union(&NeededIds::new_true()) } else { se.ids() };
|
||||
let ids = if !se.is_legal_env_chain().unwrap() { se.ids().union_into_tail(&body.ids(), Some(&id)) } else { se.ids().union(&body.ids().without(&id)) };
|
||||
let ids = if let Some(rec_under) = rec_under {
|
||||
ids.add_body_under(rec_under)
|
||||
} else {
|
||||
@@ -945,7 +966,8 @@ pub fn partial_eval(bctx_in: BCtx, dctx: DCtx, form: Rc<MarkedForm>, use_memo: b
|
||||
assert!(doublings < 100);
|
||||
last = Some(Rc::clone(&x));
|
||||
//println!("{:ident$}PE: {}", "", x, ident=dctx.ident*4);
|
||||
if !dctx.can_progress(x.ids()) {
|
||||
//if !dctx.can_progress(x.ids()) {
|
||||
if !dctx.can_progress(&x) {
|
||||
//println!("{:ident$}Shouldn't go! (because of {:?} with {:?}/{:?})", "", x.ids(), dctx.real_set, dctx.fake_set, ident=dctx.ident*4);
|
||||
if !(x.is_value() || !dctx.fake_set.is_empty()) {
|
||||
println!("Hmm what's wrong here - it's not a value, but our fake set is empty...");
|
||||
@@ -1229,11 +1251,13 @@ println!("SUSSUS deri comb");
|
||||
PushFrameResult::Ok(inner_dctx) => {
|
||||
//println!("{:ident$}Doing a body deri for {:?} which is {}", "", lookup_name, x, ident=ident_amount);
|
||||
//println!("{:ident$}and also body ids is {:?}", "", body.ids(), ident=ident_amount);
|
||||
println!("{:ident$}pushing DeriComb for {:?}", "", id, ident=ident_amount);
|
||||
// inner use doesn't count since through se
|
||||
let uses_env = bctx.get_uses_env();
|
||||
let (bctx, body) = partial_eval(bctx, inner_dctx, Rc::clone(&body), use_memo)?;
|
||||
let bctx = bctx.set_uses_env(uses_env);
|
||||
let bctx = bctx.pop_id_frame(id);
|
||||
println!("{:ident$}popping DeriComb for {:?}", "", id, ident=ident_amount);
|
||||
//println!("{:ident$}result was {}", "", body, ident=ident_amount);
|
||||
Ok((bctx, MarkedForm::new_deri_comb(se, lookup_name.clone(), de.clone(), id.clone(), *wrap_level, sequence_params.clone(), rest_params.clone(), body, None)))
|
||||
},
|
||||
@@ -1329,19 +1353,33 @@ println!("SUSSUS suspended pair");
|
||||
|
||||
//Here is where we could do a tail call instead, but there
|
||||
//would be no recovery back into the call-form...
|
||||
println!("{:ident$}pushing true derived call for for {:?}", "", id, ident=ident_amount);
|
||||
let e_mask = (de.is_some() && !e_override) || bctx.get_uses_env();
|
||||
let (bctx, r) = partial_eval(bctx.clone(), inner_dctx, Rc::clone(body), use_memo)?;
|
||||
println!("{:ident$}popping true derived call for for {:?}", "", id, ident=ident_amount);
|
||||
let newue = e_mask && bctx.get_uses_env();
|
||||
let bctx = bctx.set_uses_env(newue);
|
||||
|
||||
let mut bctx = bctx.pop_id_frame(id);
|
||||
if combiner_return_ok(&r, Some(id.clone())) {
|
||||
println!("{:ident$}return ok {:?} - {:?}", "", id, r.ids(), ident=ident_amount);
|
||||
return Ok((bctx, r));
|
||||
} else {
|
||||
if need_denv && !e_override {
|
||||
bctx = bctx.set_uses_env(true);
|
||||
}
|
||||
return Ok((bctx, MarkedForm::new_suspended_pair( saved_env, Some(r.ids()), car, cdr, None)));
|
||||
let id = id.clone();
|
||||
let car_ids = car.ids();
|
||||
let cdr_ids = cdr.ids();
|
||||
let sus_pair = MarkedForm::new_suspended_pair( saved_env, Some(r.ids()), car, cdr, None);
|
||||
println!("{:ident$}return not ok, doing sus pair {:?} - {:?} (car_ids {:?}, cdr_ids {:?})", "", id, sus_pair.ids(), car_ids, cdr_ids, ident=ident_amount);
|
||||
if r.ids().may_contain_id(&id) {
|
||||
println!("Need self to be real but we were - {}", r);
|
||||
//ok, so the not progressing when se isn't a legal env is preventing progress that could be made with a real env
|
||||
// which makes sense
|
||||
}
|
||||
assert!(!r.ids().may_contain_id(&id));
|
||||
return Ok((bctx, sus_pair));
|
||||
}
|
||||
},
|
||||
PushFrameResult::UnderBody(rec_stop_under) => { unreachable!() },
|
||||
|
||||
@@ -204,9 +204,9 @@ What were special forms in Lisp are now just built-in combiners in Kraken.
|
||||
# Motivation and examples
|
||||
|
||||
1. Vau/Combiners unify and make first class functions, macros, and built-in forms in a single simple system
|
||||
2. They are also much simpler conceptually than macro systems, which often end up quite complex (Racket has positive and negative evaluation levels, etc)
|
||||
2. They are also much simpler conceptually than macro systems while being hygenic by default
|
||||
3. Downside: naively executing a language using combiners instead of macros is exceedingly slow
|
||||
4. The code of the fexpr (analogus to a macro invocation) is re-executed at runtime, every time it is encountered
|
||||
4. The code of the operative combiner (analogus to a macro invocation) is re-executed at runtime, every time it is encountered
|
||||
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.
|
||||
|
||||
---
|
||||
@@ -215,6 +215,53 @@ What were special forms in Lisp are now just built-in combiners in Kraken.
|
||||
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
|
||||
---
|
||||
# Intuition
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
# Example time!
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user