diff --git a/interpreter/src/main/scala/nl/soqua/lcpi/interpreter/transformation/Stringify.scala b/interpreter/src/main/scala/nl/soqua/lcpi/interpreter/transformation/Stringify.scala index b8c8f5d..8d3328d 100644 --- a/interpreter/src/main/scala/nl/soqua/lcpi/interpreter/transformation/Stringify.scala +++ b/interpreter/src/main/scala/nl/soqua/lcpi/interpreter/transformation/Stringify.scala @@ -7,9 +7,36 @@ import scala.language.implicitConversions object Stringify { implicit def expression2String(e: Expression): String = Stringify(e) - def apply(e: Expression): String = e match { - case v: Variable => v.symbol - case Application(t, s) => s"(${apply(t)} ${apply(s)})" - case LambdaAbstraction(x, a) => s"(λ${apply(x)}.${apply(a)})" + private sealed trait ExpressionType + + private case object Function extends ExpressionType + + private case object Argument extends ExpressionType + + // Mark the parent as nothing special + private case object Nothing extends ExpressionType + + def apply(e: Expression): String = display(Nothing, e)("") + + private def display(parent: ExpressionType, e: Expression): String => String = (parent, e) match { + case (Function, LambdaAbstraction(_, _)) => parenthesize(display(Nothing, e)) + case (_, LambdaAbstraction(x, a)) => lambda compose display(Nothing, x) compose dot compose display(Nothing, a) + case (Function, Application(_: Variable, _: Variable)) => display(Nothing, e) + case (Function, Application(_, _)) => parenthesize(display(Nothing, e)) + case (Argument, Application(_, _)) => parenthesize(display(Nothing, e)) + case (_, Application(t, s)) => display(Function, t) compose space compose display(Argument, s) + case (_, v: Variable) => show(v.symbol) } + + private def show(s: String): String => String = (suffix: String) => + s"$s$suffix" + + private def space = show(" ") + + private def lambda = show("λ") + + private def dot = show(".") + + private def parenthesize(f: String => String): String => String = + show("(") compose f compose show(")") } diff --git a/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/IsomorphismSpec.scala b/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/IsomorphismSpec.scala index 64f5da8..bf5edf1 100644 --- a/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/IsomorphismSpec.scala +++ b/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/IsomorphismSpec.scala @@ -1,5 +1,6 @@ package nl.soqua.lcpi.interpreter.transformation +import nl.soqua.lcpi.ast.lambda.Expression import nl.soqua.lcpi.ast.lambda.Expression.A import nl.soqua.lcpi.interpreter.transformation.Stringify._ import nl.soqua.lcpi.parser.lambda.LambdaCalcParser @@ -17,6 +18,17 @@ class IsomorphismSpec extends WordSpec with Matchers { case `deBruijn` => plainIsomorphism } + private def compareWithClue(expected: Expression, actual: Expression): Assertion = { + withClue( + s""" + |expected: ${Stringify(expected)} + |actual: ${Stringify(actual)} + | + """.stripMargin) { + expected shouldBe actual + } + } + def plainIsomorphism: Assertion = { val result = for { p1 <- LambdaCalcParser(expr) @@ -24,7 +36,7 @@ class IsomorphismSpec extends WordSpec with Matchers { } yield (p1, p2) result match { case Left(ex) => fail(s"Expression $expr failed: $ex") - case Right((e1, e2)) => e1 shouldBe e2 + case Right((e1, e2)) => compareWithClue(e1, e2) } } @@ -35,7 +47,7 @@ class IsomorphismSpec extends WordSpec with Matchers { } yield (p1, p2) result match { case Left(ex) => fail(s"Expression $expr failed: $ex") - case Right((e1, e2)) => e1 shouldBe e2 + case Right((e1, e2)) => compareWithClue(e1, e2) } } } diff --git a/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/StringifySpec.scala b/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/StringifySpec.scala index 7fdc222..32422b3 100644 --- a/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/StringifySpec.scala +++ b/interpreter/src/test/scala/nl/soqua/lcpi/interpreter/transformation/StringifySpec.scala @@ -7,13 +7,20 @@ class StringifySpec extends WordSpec with Matchers { val x = V("x") val y = V("y") + val z = V("z") "Stringification of λ-expressions" should { "correctly stringify t s" in { - Stringify(A(x, y)) shouldBe "(x y)" + Stringify(A(x, y)) shouldBe "x y" } "correctly stringify t s where t is a λx.x and s is y" in { - Stringify(A(λ(x, x), y)) shouldBe "((λx.x) y)" + Stringify(A(λ(x, x), y)) shouldBe "(λx.x) y" + } + "left-recursive with application" in { + Stringify(A(A(x, y), z)) shouldBe "x y z" + } + "S-expression" in { + Stringify(λ(x, λ(y, λ(z, A(A(x, z), A(y, z)))))) shouldBe "λx.λy.λz.x z (y z)" } } } diff --git a/repl/src/test/scala/nl/soqua/lcpi/repl/monad/ReplCompilerSpec.scala b/repl/src/test/scala/nl/soqua/lcpi/repl/monad/ReplCompilerSpec.scala index 3e9d088..7975b2c 100644 --- a/repl/src/test/scala/nl/soqua/lcpi/repl/monad/ReplCompilerSpec.scala +++ b/repl/src/test/scala/nl/soqua/lcpi/repl/monad/ReplCompilerSpec.scala @@ -60,8 +60,8 @@ class ReplCompilerSpec extends ReplMonadTester with WordSpecLike with Matchers { implicit val state = emptyState.copy(traceMode = Enabled) val e = Application(Variable("I"), Variable("x")) ReplMonad.expression(e) >> List( - "S => ((λx.x) x)", - "α => ((λx.x) x)", + "S => (λx.x) x", + "α => (λx.x) x", "β => x", "η => x", "x" @@ -100,7 +100,7 @@ class ReplCompilerSpec extends ReplMonadTester with WordSpecLike with Matchers { } "render an expression in De Bruijn Index notation" in { val x = V("x") - ReplMonad.deBruijnIndex(λ(x, x)) >> "(λ.1)" + ReplMonad.deBruijnIndex(λ(x, x)) >> "λ.1" } "not be able to render a failure as de bruijn index" in { val x = V("x")