-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
remove casts from Eval, fix stack overflow in Eval #3519
Changes from 1 commit
9a81bb7
fbc4dab
fef839f
8ce139f
12eebe6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -318,52 +318,61 @@ object Eval extends EvalInstances { | |
} | ||
} | ||
|
||
private def evaluate[A](e: Eval[A]): A = { | ||
type L = Eval[Any] | ||
type M = Memoize[Any] | ||
type C = Any => Eval[Any] | ||
/* | ||
* This represents the stack of flatmap functions in a series | ||
* of Eval operations | ||
*/ | ||
sealed abstract private class FnStack[-A, +B] | ||
final private case class Ident[A, B](ev: A <:< B) extends FnStack[A, B] | ||
final private case class Many[A, B, C](first: A => Eval[B], rest: FnStack[B, C]) extends FnStack[A, C] | ||
|
||
def addToMemo(m: M): C = { (a: Any) => | ||
private def evaluate[A](e: Eval[A]): A = { | ||
def addToMemo[A1](m: Memoize[A1]): A1 => Eval[A1] = { (a: A1) => | ||
m.result = Some(a) | ||
Now(a) | ||
} | ||
|
||
@tailrec def loop(curr: L, fs: List[C]): Any = | ||
@tailrec def loop[A1](curr: Eval[A1], fs: FnStack[A1, A]): A = | ||
curr match { | ||
case c: FlatMap[_] => | ||
case c: FlatMap[A1] => | ||
val nextFs: Many[c.Start, A1, A] = Many(c.run, fs) | ||
c.start() match { | ||
case cc: FlatMap[_] => | ||
loop(cc.start().asInstanceOf[L], cc.run.asInstanceOf[C] :: c.run.asInstanceOf[C] :: fs) | ||
case cc: FlatMap[c.Start] => | ||
loop(cc.start(), Many(cc.run, nextFs)) | ||
case mm @ Memoize(eval) => | ||
mm.result match { | ||
case Some(a) => | ||
loop(Now(a), c.run.asInstanceOf[C] :: fs) | ||
loop(nextFs.first(a), nextFs.rest) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here is an optimization: since we know we already have a |
||
case None => | ||
loop(eval, addToMemo(mm.asInstanceOf[M]) :: c.run.asInstanceOf[C] :: fs) | ||
loop(eval, Many(addToMemo(mm), nextFs)) | ||
} | ||
case xx => | ||
// note: xx could be a Defer so calling .value | ||
// could blow the stack? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this looks unsafe to me. I don't see why we don't need to check for If this is safe, I think we need a comment as to why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @non do you know why we wouldn't do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, so I reproed this failure mode in a test. Should I combine with this code for the fix or make a follow up PR? |
||
loop(c.run(xx.value), fs) | ||
} | ||
case call: Defer[_] => | ||
case call: Defer[A1] => | ||
loop(advance(call), fs) | ||
case m @ Memoize(eval) => | ||
case m: Memoize[a] => | ||
// a <:< A1 | ||
m.result match { | ||
case Some(a) => | ||
fs match { | ||
case f :: fs => loop(f(a), fs) | ||
case Nil => a | ||
case Many(f, fs) => loop(f(a), fs) | ||
case Ident(ev) => ev(a) | ||
} | ||
case None => | ||
loop(eval, addToMemo(m.asInstanceOf[M]) :: fs) | ||
loop[a](m.eval, Many[a, A1, A](addToMemo[a](m), fs)) | ||
} | ||
case x => | ||
// Now, Later or Always don't have recursions | ||
fs match { | ||
case f :: fs => loop(f(x.value), fs) | ||
case Nil => x.value | ||
case Many(f, fs) => loop(f(x.value), fs) | ||
case Ident(ev) => ev(x.value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the calls to |
||
} | ||
} | ||
|
||
loop(e.asInstanceOf[L], Nil).asInstanceOf[A] | ||
loop(e, Ident(implicitly[A <:< A])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here we have 1 allocation more than we did before: |
||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure why we don't do:
and let the rest of the code run.
That would be correct, but I assume @non inlined this because for a FlatMap we know that we have at least one item on the stack and if we do my suggestion above, we allocate a
Many
just to pattern match on it in many cases and discard it. So, by nesting the pattern match on thec.start()
we often can often sometimes avoid the allocation and pattern match.If @non has time to chime in, it would be appreciated, I could update a comment to make it clearer.