diff --git a/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala b/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala index 62ac6ca..5137ff9 100644 --- a/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala +++ b/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala @@ -1,6 +1,6 @@ package me.rexim.morganey.helpers -import me.rexim.morganey.ast.{LambdaFunc, LambdaVar, LambdaTerm} +import me.rexim.morganey.ast._ import me.rexim.morganey.ast.LambdaTermHelpers._ trait TestTerms { @@ -24,6 +24,9 @@ trait TestTerms { lapp(lvar(consName), first), second)) + def binding(v1: LambdaVar, v2: LambdaVar): MorganeyBinding = + MorganeyBinding(v1, I(v2)) + val random = scala.util.Random def redex(body: LambdaTerm) = { diff --git a/src/main/scala/me/rexim/morganey/interpreter/MorganeyRepl.scala b/src/main/scala/me/rexim/morganey/interpreter/MorganeyRepl.scala index 9633d8a..030a3fb 100644 --- a/src/main/scala/me/rexim/morganey/interpreter/MorganeyRepl.scala +++ b/src/main/scala/me/rexim/morganey/interpreter/MorganeyRepl.scala @@ -20,19 +20,35 @@ class MorganeyRepl(preludeModule: Option[Module]) { case input => parseAndEval(context, line) } + // TODO: Separate REPL side effects from ReplResult + // + // Right now ReplResult.result has two meanings: + // 1. Result of the term reduction + // 2. REPL message for the user + // + // We need to separate those meanings. The proposed idea is to + // attach some kind of side effect to ReplContext and use + // ReplResult.result ONLY for term reduction results. private def parseAndEval(context: ReplContext, line: String): Computation[ReplResult[String]] = { val parseResult = Computation(LambdaParser.parseAll(LambdaParser.replCommand, line).toTry) - // TODO(#197): discriminate bindings from the rest of the nodes here and inform the user if the binding was redefined - val evaluation = parseResult flatMap { - case Success(node) => evalNode(context, node) + parseResult flatMap { + case Success(binding: MorganeyBinding) => { + Computation(ReplResult( + context.addBinding(binding), + if (!context.contains(binding)) { + None + } else { + Some(s"${binding.variable} was redefined") + } + )) + } + case Success(node) => evalNode(context, node) map (_ map smartShowTerm) case Failure(e) => Computation.failed(e) } - - evaluation map (_ map smartShowTerm) } - def evalNode(context: ReplContext, node: MorganeyNode): Computation[ReplResult[LambdaTerm]] = { + private def evalNode(context: ReplContext, node: MorganeyNode): Computation[ReplResult[LambdaTerm]] = { node match { case MorganeyLoading(Some(modulePath)) => { new Module(CanonicalPath(modulePath), preludeModule).load() match { @@ -44,11 +60,6 @@ class MorganeyRepl(preludeModule: Option[Module]) { case MorganeyLoading(None) => Computation.failed(new IllegalArgumentException("Module path was not specified!")) - // TODO(#197): separate bindings evaluation from MorganeyRepl.evalNode - case binding: MorganeyBinding => { - Computation(ReplResult(context.addBinding(binding), None)) - } - case term: LambdaTerm => term.addBindings(context.bindings).right.map { t => t.norReduceComputation().map { resultTerm => diff --git a/src/main/scala/me/rexim/morganey/interpreter/ReplContext.scala b/src/main/scala/me/rexim/morganey/interpreter/ReplContext.scala index fc6f2e6..6654560 100644 --- a/src/main/scala/me/rexim/morganey/interpreter/ReplContext.scala +++ b/src/main/scala/me/rexim/morganey/interpreter/ReplContext.scala @@ -10,18 +10,29 @@ object ReplContext { } case class ReplContext(bindings: List[MorganeyBinding] = Nil) { + def contains(binding: MorganeyBinding): Boolean = + bindings.find(_.variable.name == binding.variable.name).isDefined + // TODO(#360): Improve time asymptotic of the add binding to REPL context operation // // Right now it's O(N), but it can be improved + /** Adds a binding to the context. + * + * If a binding for a given variable already exists in the context, + * the binding is replaced with the new one. + */ def addBinding(binding: MorganeyBinding): ReplContext = { ReplContext(binding :: bindings.filter(_.variable != binding.variable)) } + /** Adds several bindings to the context. + * + * If a binding for a given variable already exists in the context, + * the binding is replaced with the new one. + */ def addBindings(newBindings: List[MorganeyBinding]): ReplContext = newBindings.foldLeft(this)(_.addBinding(_)) - def clear(): ReplContext = ReplContext(List()) - def removeBindings(predicate: MorganeyBinding => Boolean): (ReplContext, List[MorganeyBinding])= { val (satisfyF, notSatisfyF) = bindings.partition(predicate) (ReplContext(satisfyF), notSatisfyF) diff --git a/src/test/scala/me/rexim/morganey/interpreter/ReplContextSpecs.scala b/src/test/scala/me/rexim/morganey/interpreter/ReplContextSpecs.scala index 5dc6eb0..53e2dc2 100644 --- a/src/test/scala/me/rexim/morganey/interpreter/ReplContextSpecs.scala +++ b/src/test/scala/me/rexim/morganey/interpreter/ReplContextSpecs.scala @@ -16,6 +16,16 @@ import scala.util._ class ReplContextSpecs extends FlatSpec with Matchers with TestTerms with MockitoSugar { behavior of "REPL context" + it should "answer if it contains a specific binding" in { + val context = ReplContext(List( + binding(x, x), + binding(y, y) + )) + + context.contains(MorganeyBinding(y, x)) should be (true) + context.contains(MorganeyBinding(z, x)) should be (false) + } + it should "allow add bindings to it" in { val binding = MorganeyBinding(x, I(x)) val context = ReplContext().addBinding(binding) @@ -23,9 +33,6 @@ class ReplContextSpecs extends FlatSpec with Matchers with TestTerms with Mockit } it should "keep the last unique binding" in { - def binding(v1: LambdaVar, v2: LambdaVar): MorganeyBinding = - MorganeyBinding(v1, I(v2)) - val context = ReplContext(List(binding(z, z))) context.addBinding(binding(x, x)).bindings should @@ -36,12 +43,6 @@ class ReplContextSpecs extends FlatSpec with Matchers with TestTerms with Mockit be (List(binding(x, z), binding(z, z))) } - it should "clear bindings on reset command" in { - val bindings = List(MorganeyBinding(m"x", m"\\x.x")) - val context = ReplContext(bindings) - context.clear().bindings.isEmpty should be (true) - } - it should "partition all known bindings" in { val zero = MorganeyBinding(m"zero", m"0") val one = MorganeyBinding(m"one", m"1")