commit 5615807951269776fbb753ba76f4ea165a82a676 Author: Timothée Floure Date: Thu Dec 5 20:21:08 2019 +0100 Import newInterpreter handout diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..640177b --- /dev/null +++ b/build.sbt @@ -0,0 +1,12 @@ +course := "progfun2" +assignment := "newInterpreter" +name := course.value + "-" + assignment.value +testSuite := "newInterpreter.RecursiveLanguageSuite" + +scalaVersion := "0.19.0-RC1" +scalacOptions ++= Seq("-deprecation") +libraryDependencies ++= Seq( + "com.novocode" % "junit-interface" % "0.11" % Test +) + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000..66a2067 Binary files /dev/null and b/grading-tests.jar differ diff --git a/project/MOOCSettings.scala b/project/MOOCSettings.scala new file mode 100644 index 0000000..ea30b36 --- /dev/null +++ b/project/MOOCSettings.scala @@ -0,0 +1,25 @@ +package ch.epfl.lamp + +import sbt._ +import sbt.Keys._ + +/** + * Settings shared by all assignments, reused in various tasks. + */ +object MOOCSettings extends AutoPlugin { + + object autoImport { + val course = SettingKey[String]("course") + val assignment = SettingKey[String]("assignment") + val testSuite = SettingKey[String]("testSuite") + val options = SettingKey[Map[String, Map[String, String]]]("options") + } + + override def trigger = allRequirements + + override val projectSettings: Seq[Def.Setting[_]] = Seq( + parallelExecution in Test := false, + // Report test result after each test instead of waiting for every test to finish + logBuffered in Test := false + ) +} diff --git a/project/StudentTasks.scala b/project/StudentTasks.scala new file mode 100644 index 0000000..587ba85 --- /dev/null +++ b/project/StudentTasks.scala @@ -0,0 +1,323 @@ +package ch.epfl.lamp + +import sbt._ +import Keys._ + +// import scalaj.http._ +import java.io.{File, FileInputStream, IOException} +import java.nio.file.FileSystems +import org.apache.commons.codec.binary.Base64 +// import play.api.libs.json.{Json, JsObject, JsPath} +import scala.util.{Failure, Success, Try} + +import MOOCSettings.autoImport._ + +case class AssignmentInfo( + key: String, + itemId: String, + premiumItemId: Option[String], + partId: String +) + +/** + * Provides tasks for submitting the assignment + */ +object StudentTasks extends AutoPlugin { + + object autoImport { + val assignmentInfo = SettingKey[AssignmentInfo]("assignmentInfo") + + val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project") + val packageBinWithoutResources = TaskKey[File]("packageBinWithoutResources", "Like packageBin, but without the resources") + val packageSubmissionZip = TaskKey[File]("packageSubmissionZip") + val packageSubmission = inputKey[Unit]("package solution as an archive file") + val runGradingTests = taskKey[Unit]("run black-box tests used for final grading") + } + + + import autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + // submitSetting, // FIXME: restore assignmentInfo setting on assignments + runGradingTestsSettings, + + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput) + ) ++ packageSubmissionZipSettings + + lazy val runGradingTestsSettings = runGradingTests := { + val testSuiteJar = "grading-tests.jar" + if (!new File(testSuiteJar).exists) { + throw new MessageOnlyException(s"Could not find tests JarFile: $testSuiteJar") + } + + val classPath = s"${(Test / dependencyClasspath).value.map(_.data).mkString(File.pathSeparator)}${File.pathSeparator}$testSuiteJar" + val junitProcess = + Fork.java.fork( + ForkOptions(), + "-cp" :: classPath :: + "org.junit.runner.JUnitCore" :: + (Test / testSuite).value :: + Nil + ) + + // Wait for tests to complete. + junitProcess.exitValue() + } + + + /** ********************************************************** + * SUBMITTING A SOLUTION TO COURSERA + */ + + val packageSubmissionZipSettings = Seq( + packageSubmissionZip := { + val submission = crossTarget.value / "submission.zip" + val sources = (packageSourcesOnly in Compile).value + val binaries = (packageBinWithoutResources in Compile).value + IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission) + submission + }, + artifactClassifier in packageSourcesOnly := Some("sources"), + artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources")) + ) ++ + inConfig(Compile)( + Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++ + Defaults.packageTaskSettings(packageBinWithoutResources, Def.task { + val relativePaths = + (unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_)) + (mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) } + }) + ) + + val maxSubmitFileSize = { + val mb = 1024 * 1024 + 10 * mb + } + + /** Check that the jar exists, isn't empty, isn't crazy big, and can be read + * If so, encode jar as base64 so we can send it to Coursera + */ + def prepareJar(jar: File, s: TaskStreams): String = { + val errPrefix = "Error submitting assignment jar: " + val fileLength = jar.length() + if (!jar.exists()) { + s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength == 0L) { + s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength > maxSubmitFileSize) { + s.log.error(errPrefix + "jar archive is too big. Allowed size: " + + maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" + + jar.getAbsolutePath) + failSubmit() + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(jar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString) + failSubmit() + } + if (sizeRead != bytes.length) { + s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead) + failSubmit() + } else encodeBase64(bytes) + } + } + + /** Task to package solution to a given file path */ + lazy val packageSubmissionSetting = packageSubmission := { + val args: Seq[String] = Def.spaceDelimited("[path]").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val base64Jar = prepareJar(jar, s) + + val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath) + scala.tools.nsc.io.File(path).writeAll(base64Jar) + } + +/* + /** Task to submit a solution to coursera */ + val submit = inputKey[Unit]("submit solution to Coursera") + lazy val submitSetting = submit := { + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = assignmentInfo.value + val assignmentKey = assignmentDetails.key + val courseName = + course.value match { + case "capstone" => "scala-capstone" + case "bigdata" => "scala-spark-big-data" + case other => other + } + + val partId = assignmentDetails.partId + val itemId = assignmentDetails.itemId + val premiumItemId = assignmentDetails.premiumItemId + + val (email, secret) = args match { + case email :: secret :: Nil => + (email, secret) + case _ => + val inputErr = + s"""|Invalid input to `submit`. The required syntax for `submit` is: + |submit + | + |The submit token is NOT YOUR LOGIN PASSWORD. + |It can be obtained from the assignment page: + |https://www.coursera.org/learn/$courseName/programming/$itemId + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + """.stripMargin + s.log.error(inputErr) + failSubmit() + } + + val base64Jar = prepareJar(jar, s) + val json = + s"""|{ + | "assignmentKey":"$assignmentKey", + | "submitterEmail":"$email", + | "secret":"$secret", + | "parts":{ + | "$partId":{ + | "output":"$base64Jar" + | } + | } + |}""".stripMargin + + def postSubmission[T](data: String): Try[HttpResponse[String]] = { + val http = Http("https://www.coursera.org/api/onDemandProgrammingScriptSubmissions.v1") + val hs = List( + ("Cache-Control", "no-cache"), + ("Content-Type", "application/json") + ) + s.log.info("Connecting to Coursera...") + val response = Try(http.postData(data) + .headers(hs) + .option(HttpOptions.connTimeout(10000)) // scalaj default timeout is only 100ms, changing that to 10s + .asString) // kick off HTTP POST + response + } + + val connectMsg = + s"""|Attempting to submit "${assignment.value}" assignment in "$courseName" course + |Using: + |- email: $email + |- submit token: $secret""".stripMargin + s.log.info(connectMsg) + + def reportCourseraResponse(response: HttpResponse[String]): Unit = { + val code = response.code + val respBody = response.body + + /* Sample JSON response from Coursera + { + "message": "Invalid email or token.", + "details": { + "learnerMessage": "Invalid email or token." + } + } + */ + + // Success, Coursera responds with 2xx HTTP status code + if (response.is2xx) { + val successfulSubmitMsg = + s"""|Successfully connected to Coursera. (Status $code) + | + |Assignment submitted successfully! + | + |You can see how you scored by going to: + |https://www.coursera.org/learn/$courseName/programming/$itemId/ + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + |and clicking on "My Submission".""".stripMargin + s.log.info(successfulSubmitMsg) + } + + // Failure, Coursera responds with 4xx HTTP status code (client-side failure) + else if (response.is4xx) { + val result = Try(Json.parse(respBody)).toOption + val learnerMsg = result match { + case Some(resp: JsObject) => + (JsPath \ "details" \ "learnerMessage").read[String].reads(resp).get + case Some(x) => // shouldn't happen + "Could not parse Coursera's response:\n" + x + case None => + "Could not parse Coursera's response:\n" + respBody + } + val failedSubmitMsg = + s"""|Submission failed. + |There was something wrong while attempting to submit. + |Coursera says: + |$learnerMsg (Status $code)""".stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera responds with 5xx HTTP status code (server-side failure) + else if (response.is5xx) { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera seems to be unavailable at the moment (Status $code) + |Check https://status.coursera.org/ and try again in a few minutes. + """.stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera repsonds with an unexpected status code + else { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera replied with an unexpected code (Status $code) + """.stripMargin + s.log.error(failedSubmitMsg) + } + } + + // kick it all off, actually make request + postSubmission(json) match { + case Success(resp) => reportCourseraResponse(resp) + case Failure(e) => + val failedConnectMsg = + s"""|Connection to Coursera failed. + |There was something wrong while attempting to connect to Coursera. + |Check your internet connection. + |${e.toString}""".stripMargin + s.log.error(failedConnectMsg) + } + + } +*/ + + def failSubmit(): Nothing = { + sys.error("Submission failed") + } + + /** + * ***************** + * DEALING WITH JARS + */ + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c0bab04 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..f7847f8 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,7 @@ +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.3.0" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.6.9" + diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..64a2492 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "2.0.0-RC3-5") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.3.4") diff --git a/src/main/scala/newInterpreter/Logger.scala b/src/main/scala/newInterpreter/Logger.scala new file mode 100644 index 0000000..617c782 --- /dev/null +++ b/src/main/scala/newInterpreter/Logger.scala @@ -0,0 +1,16 @@ +package newInterpreter + +object Logger + private var logging = false + private var indentation = 0 + def on(): Unit = + logging = true + indentation = 0 + def off(): Unit = + logging = false + def indent(): Unit = + indentation = indentation + 1 + def unindent(): Unit = + indentation = indentation - 1 + def log(s: => String): Unit = + if logging then println("| " * indentation + s) diff --git a/src/main/scala/newInterpreter/RecursiveLanguage.scala b/src/main/scala/newInterpreter/RecursiveLanguage.scala new file mode 100644 index 0000000..5a9cc99 --- /dev/null +++ b/src/main/scala/newInterpreter/RecursiveLanguage.scala @@ -0,0 +1,226 @@ +package newInterpreter + +object RecursiveLanguage { + /** Expression tree, also called Abstract Syntax Tree (AST) */ + enum Expr + case Constant(value: Int) + case Name(name: String) + case BinOp(op: BinOps, arg1: Expr, arg2: Expr) + case IfNonzero(cond: Expr, caseTrue: Expr, caseFalse: Expr) + case Call(function: Expr, arg: Expr) + case Fun(param: String, body: Expr) + + /** The empty list, also known as nil. */ + case Empty + + /** A compound data type composed of a head and a tail. */ + case Cons(head: Expr, tail: Expr) + + /** A pattern matching expression for Empty and Cons. */ + case Match(scrutinee: Expr, caseEmpty: Expr, headName: String, tailName: String, caseCons: Expr) + import Expr._ + + /** Primitive operations that operation on constant values. */ + enum BinOps + case Plus, Minus, Times, DividedBy, Modulo, LessEq + import BinOps._ + + def evalBinOp(op: BinOps)(ex: Expr, ey: Expr): Expr = + (op, ex, ey) match + case (Plus, Constant(x), Constant(y)) => Constant(x + y) + case (Minus, Constant(x), Constant(y)) => Constant(x - y) + case (Times, Constant(x), Constant(y)) => Constant(x * y) + case (LessEq, Constant(x), Constant(y)) => Constant(if x <= y then 1 else 0) + case (Modulo, Constant(x), Constant(y)) => if y == 0 then error("Division by zero") else Constant(x % y) + case (DividedBy, Constant(x), Constant(y)) => if y == 0 then error("Division by zero") else Constant(x / y) + case _ => error(s"Type error in ${show(BinOp(op, ex, ey))}") + + type DefEnv = Map[String, Expr] + + /** Evaluates a progam e given a set of top level definition defs */ + def eval(e: Expr, defs: DefEnv): Expr = + e match + case Constant(c) => e + case Name(n) => + defs.get(n) match + case None => error(s"Unknown name $n") + case Some(body) => eval(body, defs) + case BinOp(op, e1, e2) => + evalBinOp(op)(eval(e1, defs), eval(e2, defs)) + case IfNonzero(cond, caseTrue, caseFalse) => + if eval(cond, defs) != Constant(0) then eval(caseTrue, defs) + else eval(caseFalse, defs) + case Fun(n, body) => e + case Call(fun, arg) => + Logger.log(show(e)) + Logger.indent() + val eFun = eval(fun, defs) + val eArg = eval(arg, defs) + eFun match + case Fun(n, body) => + Logger.unindent() + Logger.log(s"FUN: ${show(eFun)} ARG: ${show(eArg)}") + val bodySub = subst(body, n, eArg) + Logger.log(s"${show(bodySub)}") + Logger.indent() + val res = eval(bodySub, defs) + Logger.unindent() + Logger.log(s"+--> ${show(res)}") + res + case _ => error(s"Cannot apply non-function ${show(eFun)} in a call") + + + /** Substitutes Name(n) by r in e. */ + def subst(e: Expr, n: String, r: Expr): Expr = + e match + case Constant(c) => e + case Name(s) => if s == n then r else e + case BinOp(op, e1, e2) => + BinOp(op, subst(e1, n, r), subst(e2, n, r)) + case IfNonzero(cond, trueE, falseE) => + IfNonzero(subst(cond, n, r), subst(trueE, n, r), subst(falseE, n, r)) + case Call(f, arg) => + Call(subst(f, n, r), subst(arg, n, r)) + case Fun(param, body) => + // If n conflicts with param, there cannot be a reference to n in the + // function body, there is nothing so substite. + if param == n then e + else + val fvs = freeVars(r) + // If the free variables in r contain param the naive substitution would + // change the meaning of param to reference to the function parameter. + if fvs.contains(param) then + // Perform alpha conversion in body to eliminate the name collision. + val param1 = differentName(param, fvs) + val body1 = alphaConvert(body, param, param1) + Fun(param1, subst(body1, n, r)) + else + // Otherwise, substitute in the function body anyway. + Fun(param, subst(body, n, r)) + case Empty => ??? + case Cons(head, tail) => ??? + case Match(scrutinee, caseEmpty, headName, tailName, caseCons) => + if headName == n || tailName == n then + // If n conflicts with headName or tailName, there cannot be a reference + // to n in caseCons. Simply substite n by r in scrutinee and caseEmpty. + ??? + else + // If the free variables in r contain headName or tailName, the naive + // substitution would change their meaning to reference to pattern binds. + + // Perform alpha conversion in caseCons to eliminate the name collision. + + // Otherwise, substitute in scrutinee, caseEmpty & caseCons anyway. + ??? + + def differentName(n: String, s: Set[String]): String = + if s.contains(n) then differentName(n + "'", s) + else n + + /** Computes the set of free variable in e. */ + def freeVars(e: Expr): Set[String] = + e match + case Constant(c) => Set() + case Name(s) => Set(s) + case BinOp(op, e1, e2) => freeVars(e1) ++ freeVars(e2) + case IfNonzero(cond, trueE, falseE) => freeVars(cond) ++ freeVars(trueE) ++ freeVars(falseE) + case Call(f, arg) => freeVars(f) ++ freeVars(arg) + case Fun(param, body) => freeVars(body) - param + // TODO: Add cases for Empty, Cons & Match + + /** Substitutes Name(n) by Name(m) in e. */ + def alphaConvert(e: Expr, n: String, m: String): Expr = + e match + case Constant(c) => e + case Name(s) => if s == n then Name(m) else e + case BinOp(op, e1, e2) => + BinOp(op, alphaConvert(e1, n, m), alphaConvert(e2, n, m)) + case IfNonzero(cond, trueE, falseE) => + IfNonzero(alphaConvert(cond, n, m), alphaConvert(trueE, n, m), alphaConvert(falseE, n, m)) + case Call(f, arg) => + Call(alphaConvert(f, n, m), alphaConvert(arg, n, m)) + case Fun(param, body) => + // If n conflicts with param, there cannot be references to n in body, + // as these would reference param instead. + if param == n then e + else Fun(param, alphaConvert(body, n, m)) + // TODO: Add cases for Empty, Cons & Match + + case class EvalException(msg: String) extends Exception(msg) + + def error(msg: String) = throw EvalException(msg) + + // Printing and displaying + + /** Pretty print an expression as a String. */ + def show(e: Expr): String = + e match + case Constant(c) => c.toString + case Name(n) => n + case BinOp(op, e1, e2) => + val opString = op match + case Plus => "+" + case Minus => "-" + case Times => "*" + case DividedBy => "/" + case Modulo => "%" + case LessEq => "<=" + s"($opString ${show(e1)} ${show(e2)})" + case IfNonzero(cond, caseTrue, caseFalse) => + s"(if ${show(cond)} then ${show(caseTrue)} else ${show(caseFalse)})" + case Call(f, arg) => show(f) + "(" + show(arg) + ")" + case Fun(n, body) => s"($n => ${show(body)})" + + /** Pretty print top-level definition as a String. */ + def showEnv(env: Map[String, Expr]): String = + env.map { case (name, body) => s"def $name =\n ${show(body)}" }.mkString("\n\n") + "\n" + + /** Evaluates an expression with the given top-level definitions with logging enabled. */ + def tracingEval(e: Expr, defs: DefEnv): Expr = + Logger.on() + val evaluated = eval(e, defs) + println(s" ~~> $evaluated\n") + Logger.off() + evaluated + + def minus(e1: Expr, e2: Expr) = BinOp(BinOps.Minus, e1, e2) + def plus(e1: Expr, e2: Expr) = BinOp(BinOps.Plus, e1, e2) + def leq(e1: Expr, e2: Expr) = BinOp(BinOps.LessEq, e1, e2) + def times(e1: Expr, e2: Expr) = BinOp(BinOps.Times, e1, e2) + def modulo(e1: Expr, e2: Expr) = BinOp(BinOps.Modulo, e1, e2) + def dividedBy(e1: Expr, e2: Expr) = BinOp(BinOps.DividedBy, e1, e2) + // The {Name => N} import syntax renames Name to N in this scope + import Expr.{Name => N, Constant => C, _} + + /** Examples of top-level definitions (used in tests) */ + val definitions: DefEnv = Map[String, Expr]( + "fact" -> Fun("n", + IfNonzero(N("n"), + times(N("n"), + Call(N("fact"), minus(N("n"), C(1)))), + C(1))), + + "square" -> Fun("x", + times(N("x"), N("x"))), + + "twice" -> Fun("f", Fun("x", + Call(N("f"), Call(N("f"), N("x"))))), + + // TODO Implement map (see recitation session) + "map" -> Empty, + + // TODO Implement gcd (see recitation session) + "gcd" -> Empty, + + // TODO Implement foldLeft (see recitation session) + "foldLeft" -> Empty, + + // TODO Implement foldRight (analogous to foldLeft, but operate right-to-left) + "foldRight" -> Empty, + ) + + def main(args: Array[String]): Unit = + println(showEnv(definitions)) + tracingEval(Call(N("fact"), C(6)), definitions) + tracingEval(Call(Call(N("twice"), N("square")), C(3)), definitions) +} diff --git a/src/test/scala/newInterpreter/RecursiveLanguageSuite.scala b/src/test/scala/newInterpreter/RecursiveLanguageSuite.scala new file mode 100644 index 0000000..cd22555 --- /dev/null +++ b/src/test/scala/newInterpreter/RecursiveLanguageSuite.scala @@ -0,0 +1,126 @@ +package newInterpreter + +class RecursiveLanguageSuite { + import org.junit._ + import org.junit.Assert.{assertEquals, fail} + import RecursiveLanguage._, Expr.{Constant => C, Name => N, _} + + @Test def gcdTests(): Unit = { + def test(a: Int, b: Int, c: Int): Unit = { + val call = Call(Call(N("gcd"), C(a)), C(b)) + val result = eval(call, definitions) + assertEquals(C(c), result) + } + test(60, 90, 30) + test(36, 48, 12) + test(25, 85, 5 ) + test(12, 15, 3 ) + test(60, 75, 15) + test(35, 56, 7 ) + test(96, 128, 32) + test(120, 135, 15) + test(150, 225, 75) + } + + @Test def evalTests: Unit = { + assertEquals(Empty, eval(Empty, Map())) + assertEquals(Cons(C(1), Empty), eval(Cons(C(1), Empty), Map())) + assertEquals(Cons(C(2), Empty), eval(Cons(BinOp(BinOps.Plus, C(1), C(1)), Empty), Map())) + + assertEquals(C(0), eval(Match(Empty, C(0), "_", "_", C(1)), Map())) + assertEquals(C(1), eval(Match(Cons(Empty, Empty), C(0), "_", "_", C(1)), Map())) + assertEquals(C(2), eval(Match(Cons(C(2), Empty), C(0), "x", "xs", N("x")), Map())) + assertEquals(Empty, eval(Match(Cons(C(2), Empty), C(0), "x", "xs", N("xs")), Map())) + + try { + eval(Match(C(0), C(0), "_", "_", C(1)), Map()) + fail() + } catch { + case EvalException(msg) => // OK! + } + } + + @Test def freeVarsTests: Unit = { + assertEquals(Set(), freeVars(Empty)) + assertEquals(Set("a", "b"), freeVars(Cons(N("a"), N("b")))) + assertEquals(Set(), freeVars(Match(Empty, C(0), "a", "b", N("a")))) + assertEquals(Set(), freeVars(Match(Empty, C(0), "a", "b", N("b")))) + assertEquals(Set("c"), freeVars(Match(Empty, N("c"), "_", "_", C(1)))) + } + + @Test def alphaConvertTests: Unit = { + assertEquals(Cons(N("b"), N("b")), alphaConvert(Cons(N("a"), N("a")), "a", "b")) + assertEquals(Match(Empty, C(0), "a", "b", N("a")), alphaConvert(Match(Empty, C(0), "a", "b", N("a")), "a", "b")) + assertEquals(Match(Empty, C(0), "a", "b", N("b")), alphaConvert(Match(Empty, C(0), "a", "b", N("b")), "a", "b")) + assertEquals(Match(Empty, N("a"), "a", "b", C(1)), alphaConvert(Match(Empty, N("c"), "a", "b", C(1)), "c", "a")) + assertEquals(Match(Empty, N("b"), "a", "b", C(1)), alphaConvert(Match(Empty, N("c"), "a", "b", C(1)), "c", "b")) + assertEquals(Match(Empty, C(0), "a", "b", N("e")), alphaConvert(Match(Empty, C(0), "a", "b", N("d")), "d", "e")) + } + + val sum = "sum" -> Fun("a", Fun("b", BinOp(BinOps.Plus, N("a"), N("b")))) + val div = "div" -> Fun("a", Fun("b", BinOp(BinOps.DividedBy, N("a"), N("b")))) + + @Test def foldLeft: Unit = { + val list1 = Cons(C(1), Cons(C(2), Cons(C(3), Empty))) + val call1 = Call(Call(Call(N("foldLeft"), list1), C(100)), N("sum")) + assertEquals(C(106), eval(call1, definitions + sum)) + + val list2 = Cons(C(1), Cons(C(2), Cons(C(3), Cons(C(4), Cons(C(5), Cons(C(6), Empty)))))) + val call2 = Call(Call(Call(N("foldLeft"), list2), C(100000)), N("div")) + assertEquals(C(138), eval(call2, definitions + div)) + } + + @Test def foldRight: Unit = { + val list1 = Cons(C(1), Cons(C(2), Cons(C(3), Empty))) + val call1 = Call(Call(Call(N("foldRight"), list1), C(100)), N("sum")) + assertEquals(C(106), eval(call1, definitions + sum)) + + val list2 = Cons(C(1000000), Cons(C(20000), Cons(C(3000), Cons(C(400), Cons(C(50), Cons(C(6), Empty)))))) + val call2 = Call(Call(Call(N("foldRight"), list2), C(1)), N("div")) + assertEquals(C(3003), eval(call2, definitions + div)) + } + + @Test def substitutionSimple: Unit = { + // Substitution should happend everywhere in the Match. In scrutinee and in caseCons: + assertEquals(Cons(C(1), Empty), eval( + Call(N("bar"), Cons(C(1), Empty)), + Map("bar" -> Fun("x", Match(N("x"), C(0), "y", "z", N("x")))) + )) + + // In scrutinee and in caseEmpty: + assertEquals(Empty, eval( + Call(N("bar"), Empty), + Map("bar" -> Fun("x", Match(N("x"), N("x"), "y", "z", C(0)))) + )) + + // But not inside caseCons when the binding name clashes with the functions name: + assertEquals(C(1), eval( + Call(N("bar"), Cons(C(1), Empty)), + Map("bar" -> Fun("x", Match(N("x"), C(0), "x", "z", N("x")))) + )) + } + + @Test def substitutionFunctionCapture: Unit = { + // Here comes the real fun, plus_one uses "map" as it's first argument name, + // incorrect implementation will accidentally capture the recursion in map + // turn into that name into a reference to first argument of plus_one. + val plus_one = "plus_one" -> Fun("map", BinOp(BinOps.Plus, C(1), N("map"))) + val list1 = Cons(C(1), Cons(C(2), Cons(C(3), Empty))) + val list2 = Cons(C(2), Cons(C(3), Cons(C(4), Empty))) + assertEquals(list2, eval(Call(Call(N("map"), list1), N("plus_one")), definitions + plus_one)) + } + + @Test def substitutionMatchCapture: Unit = { + // More fun, this uses "fact" as a first binding in the pattern match: + val pairMap = "pairMap" -> Fun("pair", Fun("function", + Match( + N("pair"), + Empty, + "fact", "tcaf", + Cons(Call(N("function"), N("fact")), Call(N("function"), N("tcaf")))) + )) + assertEquals(Cons(C(6), C(24)), eval(Call(Call(N("pairMap"), Cons(C(3), C(4))), N("fact")), definitions + pairMap)) + } + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(200 * 1000) +} diff --git a/student.sbt b/student.sbt new file mode 100644 index 0000000..855fa0c --- /dev/null +++ b/student.sbt @@ -0,0 +1,9 @@ +// Used for base64 encoding +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" + +// Used for Coursera submussion +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" + +// Student tasks (i.e. packageSubmission) +enablePlugins(StudentTasks)