From 9bb559a3781627ccf646c9b39a2930a7ef5f5838 Mon Sep 17 00:00:00 2001 From: Patrick Oscar Boykin Date: Sun, 25 Feb 2024 21:55:38 -1000 Subject: [PATCH 1/2] WIP: ToyIO model --- build.sbt | 4 +- .../main/scala/org/bykn/bosatsu/ToyIO.scala | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 core/src/main/scala/org/bykn/bosatsu/ToyIO.scala diff --git a/build.sbt b/build.sbt index 5f7c01913..06b4b589f 100644 --- a/build.sbt +++ b/build.sbt @@ -183,8 +183,8 @@ lazy val core = // periodically we use acyclic to ban cyclic dependencies and make compilation faster , autoCompilerPlugins := true, - addCompilerPlugin("com.lihaoyi" % "acyclic_2.13.12" % "0.3.11"), - scalacOptions += "-P:acyclic:force" + //addCompilerPlugin("com.lihaoyi" % "acyclic_2.13.12" % "0.3.11"), + //scalacOptions += "-P:acyclic:force" ).dependsOn(base) .jsSettings(commonJsSettings) diff --git a/core/src/main/scala/org/bykn/bosatsu/ToyIO.scala b/core/src/main/scala/org/bykn/bosatsu/ToyIO.scala new file mode 100644 index 000000000..c6122a233 --- /dev/null +++ b/core/src/main/scala/org/bykn/bosatsu/ToyIO.scala @@ -0,0 +1,81 @@ +package org.bykn.bosatsu + +sealed abstract class ToyIO[+E, +A] + +object ToyIO { + case class Pure[A](get: A) extends ToyIO[Nothing, A] + case class Err[E](get: E) extends ToyIO[E, Nothing] + case class FlatMap[E, A, B](init: ToyIO[E, A], fn: A => ToyIO[E, B]) extends ToyIO[E, B] + case class RecoverWith[E, E1, A](init: ToyIO[E, A], fn: E => ToyIO[E1, A]) extends ToyIO[E1, A] + /** + * fix(f) = f(fix(f)) + */ + case class ApplyFix[E, A, B](arg: A, fixed: (A => ToyIO[E, B]) => (A => ToyIO[E, B])) extends ToyIO[E, B] + + implicit class ToyIOMethods[E, A](private val io: ToyIO[E, A]) extends AnyVal { + def flatMap[E1 >: E, B](fn: A => ToyIO[E1, B]): ToyIO[E1, B] = + FlatMap(io, fn) + + def map[B](fn: A => B): ToyIO[E, B] = flatMap(a => Pure(fn(a))) + def recoverWith[E1](fn: E => ToyIO[E1, A]): ToyIO[E1, A] = + RecoverWith(io, fn) + + def run: Either[E, A] = ToyIO.run(io) + } + + val unit: ToyIO[Nothing, Unit] = Pure(()) + + def defer[E, A](io: => ToyIO[E, A]): ToyIO[E, A] = + unit.flatMap(_ => io) + + def delay[A](a: => A): ToyIO[Nothing, A] = + defer(Pure(a)) + + def raiseError[E](e: E): ToyIO[E, Nothing] = Err(e) + + // fix(f) = f(fix(f)) + def fix[E, A, B](recur: (A => ToyIO[E, B]) => (A => ToyIO[E, B])): A => ToyIO[E, B] = + (a: A) => ApplyFix(a, recur) + + sealed trait Stack[E, A, E1, A1] + + case class Done[E1, A1, E, A](ev: E1 =:= E, av: A1 =:= A) extends Stack[E1, A1, E, A] + case class FMStep[E, E1, A, B, B1](fn: A => ToyIO[E, B], stack: Stack[E, B, E1, B1]) extends Stack[E, A, E1, B1] + case class RecStep[E, E1, E2, A, B](fn: E => ToyIO[E1, A], stack: Stack[E1, A, E2, B]) extends Stack[E, A, E2, B] + + def run[E, A](io: ToyIO[E, A]): Either[E, A] = { + + @annotation.tailrec + def loop[E1, A1](arg: ToyIO[E1, A1], stack: Stack[E1, A1, E, A]): Either[E, A] = + arg match { + case p @ Pure(get) => + stack match { + case Done(_, av) => Right(av(get)) + case FMStep(fn, stack) => + loop(fn(get), stack) + case RecStep(_, stack) => + loop(p, stack) + } + case e @ Err(get) => + stack match { + case Done(ev, _) => Left(ev(get)) + case FMStep(_, stack) => + // unwind the stack + loop(e, stack) + case RecStep(fn, stack) => + loop(fn(get), stack) + } + case FlatMap(init, fn) => + loop(init, FMStep(fn, stack)) + case rw: RecoverWith[e, e1, a] => + loop(rw.init, RecStep(rw.fn, stack)) + case af: ApplyFix[e, a, b] => + // fixed(fix(fixed)) = fix(fixed) + // take a step here, + // this may never terminate, but it won't blow the stack + loop(af.fixed(fix(af.fixed))(af.arg), stack) + } + + loop(io, Done[E, A, E, A](implicitly, implicitly)) + } +} \ No newline at end of file From d9f2901af8de2db58be398147102cbb7bb739159 Mon Sep 17 00:00:00 2001 From: Patrick Oscar Boykin Date: Sun, 3 Mar 2024 10:19:57 -1000 Subject: [PATCH 2/2] Add tests --- .../main/scala/org/bykn/bosatsu/ToyIO.scala | 22 +++- .../scala/org/bykn/bosatsu/ToyIOTest.scala | 119 ++++++++++++++++++ 2 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 core/src/test/scala/org/bykn/bosatsu/ToyIOTest.scala diff --git a/core/src/main/scala/org/bykn/bosatsu/ToyIO.scala b/core/src/main/scala/org/bykn/bosatsu/ToyIO.scala index c6122a233..d13f39a34 100644 --- a/core/src/main/scala/org/bykn/bosatsu/ToyIO.scala +++ b/core/src/main/scala/org/bykn/bosatsu/ToyIO.scala @@ -10,7 +10,17 @@ object ToyIO { /** * fix(f) = f(fix(f)) */ - case class ApplyFix[E, A, B](arg: A, fixed: (A => ToyIO[E, B]) => (A => ToyIO[E, B])) extends ToyIO[E, B] + case class ApplyFix[E, A, B](arg: A, fixed: (A => ToyIO[E, B]) => (A => ToyIO[E, B])) extends ToyIO[E, B] { + def step: ToyIO[E, B] = + // it is temping to simplify fixed(fix(fixed)) == fix(fixed) + // but fix(af.fixed)(af.arg) == af + // so that doesn't make progress. We need to apply + // af.fixed at least once to create a new value + fixed(Fix(fixed))(arg) + } + case class Fix[E, A, B](fn: (A => ToyIO[E, B]) => (A => ToyIO[E, B])) extends Function1[A, ToyIO[E, B]] { + def apply(a: A): ToyIO[E,B] = ApplyFix(a, fn) + } implicit class ToyIOMethods[E, A](private val io: ToyIO[E, A]) extends AnyVal { def flatMap[E1 >: E, B](fn: A => ToyIO[E1, B]): ToyIO[E1, B] = @@ -25,6 +35,8 @@ object ToyIO { val unit: ToyIO[Nothing, Unit] = Pure(()) + def pure[A](a: A): ToyIO[Nothing, A] = Pure(a) + def defer[E, A](io: => ToyIO[E, A]): ToyIO[E, A] = unit.flatMap(_ => io) @@ -35,7 +47,7 @@ object ToyIO { // fix(f) = f(fix(f)) def fix[E, A, B](recur: (A => ToyIO[E, B]) => (A => ToyIO[E, B])): A => ToyIO[E, B] = - (a: A) => ApplyFix(a, recur) + Fix(recur) sealed trait Stack[E, A, E1, A1] @@ -72,8 +84,10 @@ object ToyIO { case af: ApplyFix[e, a, b] => // fixed(fix(fixed)) = fix(fixed) // take a step here, - // this may never terminate, but it won't blow the stack - loop(af.fixed(fix(af.fixed))(af.arg), stack) + // this may never terminate, because there is no + // promise that a general recursive function terminates, + // but it won't blow the stack + loop(af.step, stack) } loop(io, Done[E, A, E, A](implicitly, implicitly)) diff --git a/core/src/test/scala/org/bykn/bosatsu/ToyIOTest.scala b/core/src/test/scala/org/bykn/bosatsu/ToyIOTest.scala new file mode 100644 index 000000000..f9021a6ae --- /dev/null +++ b/core/src/test/scala/org/bykn/bosatsu/ToyIOTest.scala @@ -0,0 +1,119 @@ +package org.bykn.bosatsu + +import cats.Eval +import cats.data.EitherT +import org.scalacheck.{Gen, Cogen, Prop} +import org.bykn.bosatsu.ToyIO.Pure +import org.bykn.bosatsu.ToyIO.Err +import org.bykn.bosatsu.ToyIO.FlatMap +import org.bykn.bosatsu.ToyIO.ApplyFix +import org.bykn.bosatsu.ToyIO.RecoverWith + +class ToyIOTest extends munit.ScalaCheckSuite { + + def toEvalT[E, A](toyio: ToyIO[E, A]): EitherT[Eval, E, A] = + toyio match { + case Pure(get) => EitherT[Eval, E, A](Eval.now(Right(get))) + case Err(get) => EitherT[Eval, E, A](Eval.now(Left(get))) + case fm: FlatMap[e, a1, a2] => + val first = Eval.defer[Either[e, a1]](toEvalT(fm.init).value) + EitherT(first.flatMap { + case Right(a1) => + val fn1 = fm.fn.andThen { toyio => Eval.defer(toEvalT[e, a2](toyio).value) } + fn1(a1) + case Left(err) => + Eval.now(Left(err)) + }) + case rw: RecoverWith[e, e1, a] => + val first = Eval.defer[Either[e, a]](toEvalT(rw.init).value) + EitherT(first.flatMap { + case Right(a1) => Eval.now(Right(a1)) + case Left(err) => + val fn1 = rw.fn.andThen { toyio => Eval.defer(toEvalT[e1, a](toyio).value) } + fn1(err) + }) + case af: ApplyFix[e, a, b] => + lazy val fix: a => ToyIO[e, b] = + { (a: a) => af.fixed(fix)(a) } + + EitherT(Eval.defer(toEvalT(fix(af.arg)).value)) + } + + trait Move[A] { + def notLessThan(a: A): A + def notMoreThan(a: A): A + } + object Move { + implicit val moveBoolean: Move[Boolean] = + new Move[Boolean] { + def notLessThan(a: Boolean): Boolean = true + def notMoreThan(a: Boolean): Boolean = false + } + + implicit val moveByte: Move[Byte] = + new Move[Byte] { + def notLessThan(a: Byte): Byte = + if (a == Byte.MaxValue) Byte.MaxValue + else (a + 1).toByte + def notMoreThan(a: Byte): Byte = + if (a == Byte.MinValue) Byte.MinValue + else (a - 1).toByte + } + + def apply[A](implicit m: Move[A]): Move[A] = m + } + + def genToy[E: Cogen, A: Cogen: Ordering: Move](genE: Gen[E], genA: Gen[A]): Gen[ToyIO[E, A]] = { + lazy val recur = Gen.lzy(genToy(genE, genA)) + val cogenFn: Cogen[A => ToyIO[E, A]] = + Cogen(_.hashCode.toLong) + + lazy val genFix: Gen[(A => ToyIO[E, A]) => (A => ToyIO[E, A])] = + Gen.zip(genA, Gen.oneOf(true, false), genA, genE).map { case (cut, lt, result, err) => + + val ord = implicitly[Ordering[A]] + val cmpFn = + if (lt) { (a: A) => ord.lt(a, cut) } + else { (a: A) => ord.gt(a, cut) } + val step = + if (lt) { (a: A) => Move[A].notLessThan(a) } + else { (a: A) => Move[A].notMoreThan(a) } + + { recur => + (a: A) => { + if (ord.equiv(a, cut)) ToyIO.pure(result) + else if (cmpFn(a)) recur(step(a)) + else ToyIO.raiseError(err) + } + } + } + Gen.function1(Gen.function1(recur)(Cogen[A]))(cogenFn) + + Gen.frequency( + 2 -> genA.map(ToyIO.pure(_)), + 2 -> genE.map(ToyIO.raiseError(_)), + 1 -> Gen.zip(recur, Gen.function1[A, ToyIO[E, A]](recur)).map { case (io, fn) => + io.flatMap(fn) + }, + 1 -> Gen.zip(recur, Gen.function1[E, ToyIO[E, A]](recur)).map { case (io, fn) => + io.recoverWith(fn) + }, + 1 -> Gen.zip(genA, genFix).map { case (a, fn) => ToyIO.fix(fn)(a) } + ) + } + + val bytes = Gen.choose(Byte.MinValue, Byte.MaxValue) + val bools = Gen.oneOf(true, false) + + property("evaluation of ToyIO via Eval matches E=Byte, A=Byte") { + Prop.forAll(genToy(bytes, bytes)) { toyio => + assertEquals(toyio.run, toEvalT(toyio).value.value) + } + } + + property("evaluation of ToyIO via Eval matches E=Bool, A=Bool") { + Prop.forAll(genToy(bools, bools)) { toyio => + assertEquals(toyio.run, toEvalT(toyio).value.value) + } + } +} \ No newline at end of file