diff --git a/previous-exams/2021-midterm-solutions/m1.md b/previous-exams/2021-midterm/m1.md
similarity index 95%
rename from previous-exams/2021-midterm-solutions/m1.md
rename to previous-exams/2021-midterm/m1.md
index 0846974688e1aee42c5b8c7b7a7460f4aeeeb176..49f825688c2c36e8dd2daee4c7b27311635377f4 100644
--- a/previous-exams/2021-midterm-solutions/m1.md
+++ b/previous-exams/2021-midterm/m1.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m1 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m1
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m1/.gitignore b/previous-exams/2021-midterm/m1/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m1/assignment.sbt b/previous-exams/2021-midterm/m1/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m1/build.sbt b/previous-exams/2021-midterm/m1/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..e4766880cca56983c63e60a86fd6e83af3750053
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m1"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m1.M1Suite"
diff --git a/previous-exams/2021-midterm/m1/grading-tests.jar b/previous-exams/2021-midterm/m1/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..e01bbb513ff0587ef7115a1037909157c65ef0fb
Binary files /dev/null and b/previous-exams/2021-midterm/m1/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m1/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m1/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m1/project/MOOCSettings.scala b/previous-exams/2021-midterm/m1/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m1/project/StudentTasks.scala b/previous-exams/2021-midterm/m1/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m1/project/build.properties b/previous-exams/2021-midterm/m1/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m1/project/buildSettings.sbt b/previous-exams/2021-midterm/m1/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m1/project/plugins.sbt b/previous-exams/2021-midterm/m1/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m1/src/main/scala/m1/Lib.scala b/previous-exams/2021-midterm/m1/src/main/scala/m1/Lib.scala
new file mode 100644
index 0000000000000000000000000000000000000000..37ce78015dfcc3dd679e238ab5bad98903b7e03c
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/src/main/scala/m1/Lib.scala
@@ -0,0 +1,60 @@
+package m1
+
+////////////////////////////////////////
+// NO NEED TO MODIFY THIS SOURCE FILE //
+////////////////////////////////////////
+
+trait Lib {
+
+  /** If an array has `n` elements and `n < THRESHOLD`, then it should be processed sequentially */
+  final val THRESHOLD: Int = 33
+
+  /** Compute the two values in parallel
+   *
+   *  Note: Most tests just compute those two sequentially to make any bug simpler to debug
+   */
+  def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2)
+
+  /** A limited array. It only contains the required operations for this exercise. */
+  trait Arr[T] {
+    /** Get the i-th element of the array (0-based) */
+    def apply(i: Int): T
+    /** Update the i-th element of the array with the given value (0-based) */
+    def update(i: Int, x: T): Unit
+    /** Number of elements in this array */
+    def length: Int
+    /** Create a copy of this array without the first element */
+    def tail: Arr[T]
+    /** Create a copy of this array by mapping all the elements with the given function */
+    def map[U](f: T => U): Arr[U]
+  }
+
+  object Arr {
+    /** Create an array with the given elements */
+    def apply[T](xs: T*): Arr[T] = {
+      val arr: Arr[T] = Arr.ofLength(xs.length)
+      for i <- 0 until xs.length do arr(i) = xs(i)
+      arr
+    }
+
+    /** Create an array with the given length. All elements are initialized to `null`. */
+    def ofLength[T](n: Int): Arr[T] =
+      newArrOfLength(n)
+
+  }
+
+  /** Create an array with the given length. All elements are initialized to `null`. */
+  def newArrOfLength[T](n: Int): Arr[T]
+
+  /** A fractional number representing `numerator/denominator` */
+  case class Frac(numerator: Int, denominator: Int) {
+    def toDouble: Double = numerator.toDouble / denominator
+  }
+
+  /** Tree result of an upsweep operation. Specialized for `Frac` results. */
+  trait TreeRes { val res: Frac }
+  /** Leaf result of an upsweep operation. Specialized for `Frac` results. */
+  case class Leaf(from: Int, to: Int, res: Frac) extends TreeRes
+  /** Tree node result of an upsweep operation. Specialized for `Frac` results. */
+  case class Node(left: TreeRes, res: Frac, right: TreeRes) extends TreeRes
+}
diff --git a/previous-exams/2021-midterm/m1/src/main/scala/m1/M1.scala b/previous-exams/2021-midterm/m1/src/main/scala/m1/M1.scala
new file mode 100644
index 0000000000000000000000000000000000000000..8eaea55ff391cd121b054a8fe3fc9a22e3e4e089
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/src/main/scala/m1/M1.scala
@@ -0,0 +1,90 @@
+package m1
+
+
+trait M1 extends Lib {
+  // Functions and classes of Lib can be used in here
+
+  /** Compute the rolling average of array.
+   *
+   *  For an array `arr = Arr(x1, x2, x3, ..., xn)` the result is
+   *  `Arr(x1 / 1, (x1 + x2) / 2, (x1 + x2 + x3) / 3, ..., (x1 + x2 + x3 + ... + xn) / n)`
+   */
+  def rollingAveragesParallel(arr: Arr[Int]): Arr[Double] = {
+    if (arr.length == 0) return Arr.ofLength(0)
+    // TASK 1:  Add missing parallelization in `upsweep` and `downsweep`.
+    //          You should use the `parallel` method.
+    //          You should use the sequential version if the number of elements is lower than THRESHOLD.
+    // TASK 2a: Pass `arr` to `upsweep` and `downsweep` instead of `tmp`.
+    //          You will need to change some signatures and update the code appropriately.
+    //          Remove the definition of `tmp`
+    // TASK 2b: Change the type of the array `out` from `Frac` to `Double`
+    //          You will need to change some signatures and update the code appropriately.
+    //          Remove the call `.map(frac => frac.toDouble)`.
+    // TASK 3:  Remove the call to `.tail`.
+    //          Update the update the code appropriately.
+
+    val tmp: Arr[Frac] = arr.map(x => Frac(x, 1))
+    val out: Arr[Frac] = Arr.ofLength(arr.length + 1)
+    val tree = upsweep(tmp, 0, arr.length)
+    downsweep(tmp, Frac(0, 0), tree, out)
+    out(0) = Frac(0, 0)
+    out.map(frac => frac.toDouble).tail
+
+    // IDEAL SOLUTION
+    // val out = Arr.ofLength(arr.length)
+    // val tree = upsweep(arr, 0, arr.length)
+    // downsweep(arr, Frac(0, 0), tree, out)
+    // out
+  }
+
+  def scanOp(acc: Frac, x: Frac) = // No need to modify this method
+    Frac(acc.numerator + x.numerator, acc.denominator + x.denominator)
+
+  def upsweep(input: Arr[Frac], from: Int, to: Int): TreeRes = {
+    if (to - from < 2)
+      Leaf(from, to, reduceSequential(input, from + 1, to, input(from)))
+    else {
+      val mid = from + (to - from) / 2
+      val (tL, tR) = (
+        upsweep(input, from, mid),
+        upsweep(input, mid, to)
+      )
+      Node(tL, scanOp(tL.res, tR.res), tR)
+    }
+  }
+
+  def downsweep(input: Arr[Frac], a0: Frac, tree: TreeRes, output: Arr[Frac]): Unit = {
+    tree match {
+      case Node(left, _, right) =>
+        (
+          downsweep(input, a0, left, output),
+          downsweep(input, scanOp(a0, left.res), right, output)
+        )
+      case Leaf(from, to, _) =>
+        downsweepSequential(input, from, to, a0, output)
+    }
+  }
+
+  def downsweepSequential(input: Arr[Frac], from: Int, to: Int, a0: Frac, output: Arr[Frac]): Unit = {
+    if (from < to) {
+      var i = from
+      var a = a0
+      while (i < to) {
+        a = scanOp(a, input(i))
+        i = i + 1
+        output(i) = a
+      }
+    }
+  }
+
+  def reduceSequential(input: Arr[Frac], from: Int, to: Int, a0: Frac): Frac = {
+    var a = a0
+    var i = from
+    while (i < to) {
+      a = scanOp(a, input(i))
+      i = i + 1
+    }
+    a
+  }
+
+}
diff --git a/previous-exams/2021-midterm/m1/src/test/scala/m1/M1Suite.scala b/previous-exams/2021-midterm/m1/src/test/scala/m1/M1Suite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..ab1b8652a90c04eddcf5767bc413126be1152f6e
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/src/test/scala/m1/M1Suite.scala
@@ -0,0 +1,156 @@
+package m1
+
+class M1Suite extends munit.FunSuite {
+
+  test("Rolling average result test (5pts)") {
+    RollingAveragesBasicLogicTest.basicTests()
+    RollingAveragesBasicLogicTest.normalTests()
+    RollingAveragesBasicLogicTest.largeTests()
+  }
+
+  test("[TASK 1] Rolling average parallelism test (30pts)") {
+    RollingAveragesCallsToParallel.parallelismTest()
+    RollingAveragesParallel.basicTests()
+    RollingAveragesParallel.normalTests()
+    RollingAveragesParallel.largeTests()
+  }
+
+  test("[TASK 2] Rolling average no `map` test (35pts)") {
+    RollingAveragesNoMap.basicTests()
+    RollingAveragesNoMap.normalTests()
+    RollingAveragesNoMap.largeTests()
+  }
+
+  test("[TASK 3] Rolling average no `tail` test (30pts)") {
+    RollingAveragesNoTail.basicTests()
+    RollingAveragesNoTail.normalTests()
+    RollingAveragesNoTail.largeTests()
+  }
+
+
+  object RollingAveragesBasicLogicTest extends M1 with LibImpl with RollingAveragesTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+  }
+
+  object RollingAveragesCallsToParallel extends M1 with LibImpl with RollingAveragesTest {
+    private var count = 0
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) =
+      count += 1
+      (op1, op2)
+
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+
+    def parallelismTest() = {
+      assertParallelCount(Arr(), 0)
+      assertParallelCount(Arr(1), 0)
+      assertParallelCount(Arr(1, 2, 3, 4), 0)
+      assertParallelCount(Arr(Array.tabulate(16)(identity): _*), 0)
+      assertParallelCount(Arr(Array.tabulate(32)(identity): _*), 0)
+
+      assertParallelCount(Arr(Array.tabulate(33)(identity): _*), 2)
+      assertParallelCount(Arr(Array.tabulate(64)(identity): _*), 2)
+      assertParallelCount(Arr(Array.tabulate(128)(identity): _*), 6)
+      assertParallelCount(Arr(Array.tabulate(256)(identity): _*), 14)
+      assertParallelCount(Arr(Array.tabulate(1000)(identity): _*), 62)
+      assertParallelCount(Arr(Array.tabulate(1024)(identity): _*), 62)
+    }
+
+    def assertParallelCount(arr: Arr[Int], expected: Int): Unit = {
+      try {
+        count = 0
+        rollingAveragesParallel(arr)
+        assert(count == expected, {
+          val extra = if (expected == 0) "" else s" ${expected/2} for the `upsweep` and ${expected/2} for the `downsweep`"
+          s"\n$arr\n\nERROR: Expected $expected instead of $count calls to `parallel(...)` for an array of ${arr.length} elements. Current parallel threshold is $THRESHOLD.$extra"
+        })
+      } finally {
+        count = 0
+      }
+    }
+
+  }
+
+  object RollingAveragesNoMap extends M1 with LibImpl with RollingAveragesTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) {
+      override def map[U](f: T => U): Arr[U] = throw Exception("Should not call Arr.map")
+    }
+  }
+
+  object RollingAveragesNoTail extends M1 with LibImpl with RollingAveragesTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) {
+      override def tail: Arr[T] = throw Exception("Should not call Arr.tail")
+    }
+  }
+
+  object RollingAveragesParallel extends M1 with LibImpl with RollingAveragesTest {
+    import scala.concurrent.duration._
+    val TIMEOUT = Duration(10, SECONDS)
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = {
+      import concurrent.ExecutionContext.Implicits.global
+      import scala.concurrent._
+      Await.result(Future(op1).zip(Future(op2)), TIMEOUT) // FIXME not timing-out
+    }
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+  }
+
+  trait LibImpl extends Lib {
+
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T]
+
+    def newArrOfLength[T](n: Int): Arr[T] =
+      newArrFrom(new Array(n))
+
+    class ArrImpl[T](val arr: Array[AnyRef]) extends Arr[T]:
+      def apply(i: Int): T =
+        arr(i).asInstanceOf[T]
+      def update(i: Int, x: T): Unit =
+        arr(i) = x.asInstanceOf[AnyRef]
+      def length: Int =
+        arr.length
+      def map[U](f: T => U): Arr[U] =
+        newArrFrom(arr.map(f.asInstanceOf[AnyRef => AnyRef]))
+      def tail: Arr[T] =
+        newArrFrom(arr.tail)
+      override def toString: String =
+        arr.mkString("Arr(", ", ", ")")
+      override def equals(that: Any): Boolean =
+        that match
+          case that: ArrImpl[_] => Array.equals(arr, that.arr)
+          case _ => false
+  }
+
+  trait RollingAveragesTest extends M1 {
+
+    def tabulate[T](n: Int)(f: Int => T): Arr[T] =
+      val arr = Arr.ofLength[T](n)
+      for i <- 0 until n do
+        arr(i) = f(i)
+      arr
+
+    def basicTests() = {
+      assertEquals(rollingAveragesParallel(Arr()), Arr[Double]())
+      assertEquals(rollingAveragesParallel(Arr(1)), Arr[Double](1))
+      assertEquals(rollingAveragesParallel(Arr(1, 2, 3, 4)), Arr(1, 1.5, 2, 2.5))
+      assertEquals(rollingAveragesParallel(Arr(4, 4, 4, 4)), Arr[Double](4, 4, 4, 4))
+    }
+
+    def normalTests() = {
+      assertEquals(rollingAveragesParallel(Arr(Array.tabulate(64)(identity): _*)), Arr(Array.tabulate(64)(_.toDouble / 2): _*))
+      assertEquals(rollingAveragesParallel(Arr(4, 4, 4, 4)), Arr[Double](4, 4, 4, 4))
+      assertEquals(rollingAveragesParallel(Arr(4, 8, 6, 4)), Arr[Double](4, 6, 6, 5.5))
+      assertEquals(rollingAveragesParallel(Arr(4, 3, 2, 1)), Arr(4, 3.5, 3, 2.5))
+      assertEquals(rollingAveragesParallel(Arr(Array.tabulate(64)(identity).reverse: _*)), Arr(Array.tabulate(64)(i => 63 - i.toDouble / 2): _*))
+      assertEquals(rollingAveragesParallel(Arr(Array.tabulate(128)(i => 128 - 2*i).reverse: _*)), Arr(Array.tabulate(128)(i => -126d + i): _*))
+    }
+
+    def largeTests() = {
+      assertEquals(rollingAveragesParallel(Arr(Array.tabulate(500)(identity): _*)), Arr(Array.tabulate(500)(_.toDouble / 2): _*))
+      assertEquals(rollingAveragesParallel(Arr(Array.tabulate(512)(identity): _*)), Arr(Array.tabulate(512)(_.toDouble / 2): _*))
+      assertEquals(rollingAveragesParallel(Arr(Array.tabulate(1_000)(identity): _*)), Arr(Array.tabulate(1_000)(_.toDouble / 2): _*))
+      assertEquals(rollingAveragesParallel(Arr(Array.tabulate(10_000)(identity): _*)), Arr(Array.tabulate(10_000)(_.toDouble / 2): _*))
+    }
+  }
+}
\ No newline at end of file
diff --git a/previous-exams/2021-midterm-solutions/m14.md b/previous-exams/2021-midterm/m14.md
similarity index 95%
rename from previous-exams/2021-midterm-solutions/m14.md
rename to previous-exams/2021-midterm/m14.md
index 62f775d8a176774aef56064544cb4935ff71e5ee..7b0deeb4e9c7712f21d55b7a861a81927c7afa0e 100644
--- a/previous-exams/2021-midterm-solutions/m14.md
+++ b/previous-exams/2021-midterm/m14.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m14 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m14
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m14/.gitignore b/previous-exams/2021-midterm/m14/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m14/assignment.sbt b/previous-exams/2021-midterm/m14/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m14/build.sbt b/previous-exams/2021-midterm/m14/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..aeee575ac20b4770fe264f7327098c1da3387794
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m14"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m14.M14Suite"
diff --git a/previous-exams/2021-midterm/m14/grading-tests.jar b/previous-exams/2021-midterm/m14/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..9c8d6fdaac52512ffcac6882f5650c7673caa5f4
Binary files /dev/null and b/previous-exams/2021-midterm/m14/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m14/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m14/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m14/project/MOOCSettings.scala b/previous-exams/2021-midterm/m14/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m14/project/StudentTasks.scala b/previous-exams/2021-midterm/m14/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m14/project/build.properties b/previous-exams/2021-midterm/m14/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m14/project/buildSettings.sbt b/previous-exams/2021-midterm/m14/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m14/project/plugins.sbt b/previous-exams/2021-midterm/m14/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractBlockingQueue.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractBlockingQueue.scala
new file mode 100644
index 0000000000000000000000000000000000000000..a91a39c7ce364151f5c2aa9968de43aecc1ef984
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractBlockingQueue.scala
@@ -0,0 +1,14 @@
+package m14
+
+abstract class AbstractBlockingQueue[T] extends Monitor {
+  private var underlying: List[T] = Nil
+
+  def getUnderlying(): List[T] =
+    underlying
+
+  def setUnderlying(newValue: List[T]): Unit =
+    underlying = newValue
+
+  def put(elem: T): Unit
+  def take(): T
+}
diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractThreadPoolExecutor.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractThreadPoolExecutor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..670294c04eaa9376c25984061168a2c0ad275669
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/AbstractThreadPoolExecutor.scala
@@ -0,0 +1,7 @@
+package m14
+
+abstract class AbstractThreadPoolExecutor {
+  def execute(task: Unit => Unit): Unit
+  def start(): Unit
+  def shutdown(): Unit
+}
diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/M14.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/M14.scala
new file mode 100644
index 0000000000000000000000000000000000000000..18229ebdeed559f400045c0b20595394f0ac88f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/M14.scala
@@ -0,0 +1,62 @@
+package m14
+
+object M14 {
+  /** A thread pool that executes submitted task using one of several threads */
+  class ThreadPoolExecutor(taskQueue: BlockingQueue[Unit => Unit], poolSize: Int)
+      extends AbstractThreadPoolExecutor {
+
+    private class Worker extends Thread {
+      override def run(): Unit = {
+        try {
+          while (true) {
+            ???
+          }
+        } catch {
+          case e: InterruptedException =>
+            // Nothing to do here, we are shutting down gracefully.
+        }
+      }
+    }
+    private val workers: List[Worker] = List.fill(poolSize)(new Worker())
+
+    /** Executes the given task, passed by name. */
+    def execute(task: Unit => Unit): Unit =
+      ???
+
+    /** Starts the thread pool. */
+    def start(): Unit =
+      workers.foreach(_.start())
+
+    /** Instantly shuts down all actively executing tasks using an interrupt. */
+    def shutdown(): Unit =
+      workers.foreach(_.interrupt())
+  }
+
+  /**
+   * A queue whose take operations blocks until the queue become non-empty.
+   * Elements must be retrived from this queue in a last in, first out order.
+   * All methods of this class are thread safe, that is, they can safely
+   * be used from multiple thread without any particular synchronization.
+   */
+  class BlockingQueue[T] extends AbstractBlockingQueue[T] {
+
+    // The state of this queue is stored in an underlying List[T] defined in
+    // the AbstractBlockingQueue class. Your implementation should access and
+    // update this list using the following setter and getter methods:
+    // - def getUnderlying(): List[T]
+    // - def setUnderlying(newValue: List[T]): Unit
+    // Using these methods is required for testing purposes.
+
+    /** Inserts the specified element into this queue (non-blocking) */
+    def put(elem: T): Unit =
+      ???
+
+    /**
+     * Retrieves and removes the head of this queue, waiting if necessary
+     * until an element becomes available (blocking).
+     * This queue operates in a last in, first out order.
+     */
+    def take(): T =
+      ???
+  }
+}
diff --git a/previous-exams/2021-midterm/m14/src/main/scala/m14/Monitor.scala b/previous-exams/2021-midterm/m14/src/main/scala/m14/Monitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..97dd73a6038ef6966a899da8c6b1c7dc9c9109de
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/Monitor.scala
@@ -0,0 +1,23 @@
+package m14
+
+class Dummy
+
+trait Monitor {
+  implicit val dummy: Dummy = new Dummy
+
+  def wait()(implicit i: Dummy) = waitDefault()
+
+  def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e)
+
+  def notify()(implicit i: Dummy) = notifyDefault()
+
+  def notifyAll()(implicit i: Dummy) = notifyAllDefault()
+
+  private val lock = new AnyRef
+
+  // Can be overriden.
+  def waitDefault(): Unit = lock.wait()
+  def synchronizedDefault[T](toExecute: => T): T = lock.synchronized(toExecute)
+  def notifyDefault(): Unit = lock.notify()
+  def notifyAllDefault(): Unit = lock.notifyAll()
+}
diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/M14Suite.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/M14Suite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..cd04f2a32971eedc0e12400ae35f88d7c8f45571
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/M14Suite.scala
@@ -0,0 +1,159 @@
+package m14
+
+import instrumentation.SchedulableBlockingQueue
+import instrumentation.TestHelper._
+import instrumentation.TestUtils._
+
+class M14Suite extends munit.FunSuite {
+  import M14._
+
+  test("ThreadPool should put jobs in the queue, Workers should execute jobs from the queue (10pts)") {
+    case class PutE(e: Unit => Unit) extends Exception
+    val nThreads = 3
+    var taken = false
+    class TestBlockingQueue extends BlockingQueue[Unit => Unit] {
+      override def put(e: Unit => Unit): Unit =
+        throw new PutE(e)
+
+      override def take(): Unit => Unit =
+        x => {
+          taken = true
+          Thread.sleep(10 * 1000)
+        }
+    }
+
+    val tpe = new ThreadPoolExecutor(new TestBlockingQueue, nThreads)
+    val unit2unit: Unit => Unit = x => ()
+    try {
+      tpe.execute(unit2unit)
+      assert(false, "ThreadPoolExecutor does not put jobs in the queue")
+    } catch {
+      case PutE(e) =>
+        assert(e == unit2unit)
+    }
+    tpe.start()
+    Thread.sleep(1000)
+    assert(taken, s"ThreadPoolExecutor workers do no execute jobs from the queue")
+    tpe.shutdown()
+  }
+
+  test("BlockingQueue should work in a sequential setting (1pts)") {
+    testSequential[(Int, Int, Int, Int)]{ sched =>
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      queue.put(1)
+      queue.put(2)
+      queue.put(3)
+      queue.put(4)
+      (queue.take(),
+      queue.take(),
+      queue.take(),
+      queue.take())
+    }{ tuple =>
+      (tuple == (4, 3, 2, 1), s"Expected (4, 3, 2, 1) got $tuple")
+    }
+  }
+
+  test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'take' (3pts)") {
+    testManySchedules(2, sched => {
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      (List(() => queue.put(1), () => queue.take()),
+       args => (args(1) == 1, s"Expected 1, got ${args(1)}"))
+    })
+  }
+
+  test("BlockingQueue should not be able to take from an empty queue (3pts)") {
+    testSequential[Boolean]{ sched =>
+      val queue = new SchedulableBlockingQueue[Int](sched);
+      queue.put(1)
+      queue.put(2)
+      queue.take()
+      queue.take()
+      failsOrTimesOut(queue.take())
+    }{ res =>
+      (res, "Was able to retrieve an element from an empty queue")
+    }
+  }
+
+  test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take' (5pts)") {
+    testManySchedules(3, sched => {
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      (List(() => queue.put(1), () => queue.put(2), () => queue.take())
+      , args => {
+        val takeRes = args(2).asInstanceOf[Int]
+        val nocreation = (takeRes == 1 || takeRes == 2)
+        if (!nocreation)
+          (false, s"'take' should return either 1 or 2")
+        else (true, "")
+      })
+    })
+  }
+
+  test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take', Thread 4: 'take' (10pts)") {
+    testManySchedules(4, sched => {
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      (List(() => queue.put(1), () => queue.put(2), () => queue.take(), () => queue.take())
+      , args => {
+        def m(): (Boolean, String) = {
+          val takeRes1 = args(2).asInstanceOf[Int]
+          val takeRes2 = args(3).asInstanceOf[Int]
+          val nocreation = (x: Int) => List(1, 2).contains(x)
+          if (!nocreation(takeRes1))
+            return (false, s"'Thread 3: take' returned $takeRes1 but should return a value in {1, 2, 3}")
+          if (!nocreation(takeRes2))
+            return (false, s"'Thread 4: take' returned $takeRes2 but should return a value in {1, 2, 3}")
+
+          val noduplication = takeRes1 != takeRes2
+          if (!noduplication)
+            (false, s"'Thread 3 and 4' returned the same value: $takeRes1")
+          else (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'put(3)', Thread 4: 'take', Thread 5: 'take' (10pts)") {
+    testManySchedules(5, sched => {
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      (List(() => queue.put(1), () => queue.put(2), () => queue.put(3),
+        () => queue.take(), () => queue.take())
+      , args => {
+        def m(): (Boolean, String) = {
+          val takeRes1 = args(3).asInstanceOf[Int]
+          val takeRes2 = args(4).asInstanceOf[Int]
+          val nocreation = (x: Int) => List(1, 2, 3).contains(x)
+          if (!nocreation(takeRes1))
+            return (false, s"'Thread 4: take' returned $takeRes1 but should return a value in {1, 2, 3}")
+          if (!nocreation(takeRes2))
+            return (false, s"'Thread 5: take' returned $takeRes2 but should return a value in {1, 2, 3}")
+
+          val noduplication = takeRes1 != takeRes2
+          if (!noduplication)
+            return (false, s"'Thread 4 and 5' returned the same value: $takeRes1")
+          else (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  test("BlockingQueue should work when Thread 1: 'put(1); put(2); take', Thread 2: 'put(3)', Thread 3: 'put(4)' (10pts)") {
+    testManySchedules(3, sched => {
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      (List(
+        () => { queue.put(1); queue.put(2); queue.take() },
+        () => queue.put(3),
+        () => queue.put(4)
+      ), args => {
+        val takeRes = args(0).asInstanceOf[Int]
+        val nocreation = List(1, 2, 3, 4).contains
+        if (!nocreation(takeRes))
+          (false, s"'Thread 1: take' returned $takeRes, but should return a value in {1, 2, 3, 4}")
+        else if (takeRes == 1)
+          (false, s"'Thread 1' returned 2 before returning 1 (got $takeRes)")
+        else
+          (true, "")
+      })
+    })
+  }
+}
diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/MockedMonitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..64aa205a5e1487a2dc0d3f6dc9f4435454a5b648
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/MockedMonitor.scala
@@ -0,0 +1,73 @@
+package m14
+package instrumentation
+
+trait MockedMonitor extends Monitor {
+  def scheduler: Scheduler
+
+  // Can be overriden.
+  override def waitDefault() = {
+    scheduler.log("wait")
+    scheduler updateThreadState Wait(this, scheduler.threadLocks.tail)
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    scheduler.log("synchronized check")
+    val prevLocks = scheduler.threadLocks
+    scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue.
+    scheduler.log("synchronized -> enter")
+    try {
+      toExecute
+    } finally {
+      scheduler updateThreadState Running(prevLocks)
+      scheduler.log("synchronized -> out")
+    }
+  }
+  override def notifyDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notify")
+  }
+  override def notifyAllDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notifyAll")
+  }
+}
+
+trait LockFreeMonitor extends Monitor {
+  override def waitDefault() = {
+    throw new Exception("Please use lock-free structures and do not use wait()")
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    throw new Exception("Please use lock-free structures and do not use synchronized()")
+  }
+  override def notifyDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notify()")
+  }
+  override def notifyAllDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notifyAll()")
+  }
+}
+
+
+abstract class ThreadState {
+  def locks: Seq[AnyRef]
+}
+trait CanContinueIfAcquiresLock extends ThreadState {
+  def lockToAquire: AnyRef
+}
+case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState
+case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Running(locks: Seq[AnyRef]) extends ThreadState
+case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState
diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/SchedulableBlockingQueue.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/SchedulableBlockingQueue.scala
new file mode 100644
index 0000000000000000000000000000000000000000..16e68fa913f770e7b85cee2af2bdec390cb7ee4e
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/SchedulableBlockingQueue.scala
@@ -0,0 +1,17 @@
+package m14
+package instrumentation
+
+class SchedulableBlockingQueue[T](val scheduler: Scheduler)
+  extends m14.M14.BlockingQueue[T] with MockedMonitor {
+    private var underlying: List[T] = Nil
+
+    override def getUnderlying(): List[T] =
+      scheduler.exec {
+        underlying
+      }(s"Get $underlying")
+
+    override def setUnderlying(newValue: List[T]): Unit =
+      scheduler.exec {
+        underlying = newValue
+      }(s"Set $newValue")
+}
diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Scheduler.scala
new file mode 100644
index 0000000000000000000000000000000000000000..448a8091eed701a6258b53110aef2ae17416afc8
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Scheduler.scala
@@ -0,0 +1,305 @@
+package m14
+package instrumentation
+
+import java.util.concurrent._;
+import scala.concurrent.duration._
+import scala.collection.mutable._
+import Stats._
+
+import java.util.concurrent.atomic.AtomicInteger
+
+sealed abstract class Result
+case class RetVal(rets: List[Any]) extends Result
+case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result
+case class Timeout(msg: String) extends Result
+
+/**
+ * A class that maintains schedule and a set of thread ids.
+ * The schedules are advanced after an operation of a SchedulableBuffer is performed.
+ * Note: the real schedule that is executed may deviate from the input schedule
+ * due to the adjustments that had to be made for locks
+ */
+class Scheduler(sched: List[Int]) {
+  val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform
+
+  private var schedule = sched
+  private var numThreads = 0
+  private val realToFakeThreadId = Map[Long, Int]()
+  private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat)
+  private val threadStates = Map[Int, ThreadState]()
+
+  /**
+   * Runs a set of operations in parallel as per the schedule.
+   * Each operation may consist of many primitive operations like reads or writes
+   * to shared data structure each of which should be executed using the function `exec`.
+   * @timeout in milliseconds
+   * @return true - all threads completed on time,  false -some tests timed out.
+   */
+  def runInParallel(timeout: Long, ops: List[() => Any]): Result = {
+    numThreads = ops.length
+    val threadRes = Array.fill(numThreads) { None: Any }
+    var exception: Option[Except] = None
+    val syncObject = new Object()
+    var completed = new AtomicInteger(0)
+    // create threads
+    val threads = ops.zipWithIndex.map {
+      case (op, i) =>
+        new Thread(new Runnable() {
+          def run(): Unit = {
+            val fakeId = i + 1
+            setThreadId(fakeId)
+            try {
+              updateThreadState(Start)
+              val res = op()
+              updateThreadState(End)
+              threadRes(i) = res
+              // notify the master thread if all threads have completed
+              if (completed.incrementAndGet() == ops.length) {
+                syncObject.synchronized { syncObject.notifyAll() }
+              }
+            } catch {
+              case e: Throwable if exception != None => // do nothing here and silently fail
+              case e: Throwable =>
+                log(s"throw ${e.toString}")
+                exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"),
+                    e.getStackTrace))
+                syncObject.synchronized { syncObject.notifyAll() }
+              //println(s"$fakeId: ${e.toString}")
+              //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads)
+            }
+          }
+        })
+    }
+    // start all threads
+    threads.foreach(_.start())
+    // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire
+    var remTime = timeout
+    syncObject.synchronized {
+      timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time }
+    }
+    if (exception.isDefined) {
+      exception.get
+    } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors
+      Timeout(opLog.mkString("\n"))
+    } else {
+      // every thing executed normally
+      RetVal(threadRes.toList)
+    }
+  }
+
+  // Updates the state of the current thread
+  def updateThreadState(state: ThreadState): Unit = {
+    val tid = threadId
+    synchronized {
+      threadStates(tid) = state
+    }
+    state match {
+      case Sync(lockToAquire, locks) =>
+        if (locks.indexOf(lockToAquire) < 0) waitForTurn else {
+          // Re-aqcuiring the same lock
+          updateThreadState(Running(lockToAquire +: locks))
+        }
+      case Start      => waitStart()
+      case End        => removeFromSchedule(tid)
+      case Running(_) =>
+      case _          => waitForTurn // Wait, SyncUnique, VariableReadWrite
+    }
+  }
+
+  def waitStart(): Unit = {
+    //while (threadStates.size < numThreads) {
+    //Thread.sleep(1)
+    //}
+    synchronized {
+      if (threadStates.size < numThreads) {
+        wait()
+      } else {
+        notifyAll()
+      }
+    }
+  }
+
+  def threadLocks = {
+    synchronized {
+      threadStates(threadId).locks
+    }
+  }
+
+  def threadState = {
+    synchronized {
+      threadStates(threadId)
+    }
+  }
+
+  def mapOtherStates(f: ThreadState => ThreadState) = {
+    val exception = threadId
+    synchronized {
+      for (k <- threadStates.keys if k != exception) {
+        threadStates(k) = f(threadStates(k))
+      }
+    }
+  }
+
+  def log(str: String) = {
+    if((realToFakeThreadId contains Thread.currentThread().getId())) {
+      val space = (" " * ((threadId - 1) * 2))
+      val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + "  ")
+      opLog += s
+    }
+  }
+
+  /**
+   * Executes a read or write operation to a global data structure as per the given schedule
+   * @param msg a message corresponding to the operation that will be logged
+   */
+  def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = {
+    if(! (realToFakeThreadId contains Thread.currentThread().getId())) {
+      primop
+    } else {
+      updateThreadState(VariableReadWrite(threadLocks))
+      val m = msg
+      if(m != "") log(m)
+      if (opLog.size > maxOps)
+        throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!")
+      val res = primop
+      postMsg match {
+        case Some(m) => log(m(res))
+        case None =>
+      }
+      res
+    }
+  }
+
+  private def setThreadId(fakeId: Int) = synchronized {
+    realToFakeThreadId(Thread.currentThread.getId) = fakeId
+  }
+
+  def threadId =
+    try {
+      realToFakeThreadId(Thread.currentThread().getId())
+    } catch {
+    case e: NoSuchElementException =>
+      throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!")
+    }
+
+  private def isTurn(tid: Int) = synchronized {
+    (!schedule.isEmpty && schedule.head != tid)
+  }
+
+  def canProceed(): Boolean = {
+    val tid = threadId
+    canContinue match {
+      case Some((i, state)) if i == tid =>
+        //println(s"$tid: Runs ! Was in state $state")
+        canContinue = None
+        state match {
+          case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks))
+          case SyncUnique(lockToAquire, locks) =>
+            mapOtherStates {
+              _ match {
+                case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2)
+                case e => e
+              }
+            }
+            updateThreadState(Running(lockToAquire +: locks))
+          case VariableReadWrite(locks) => updateThreadState(Running(locks))
+        }
+        true
+      case Some((i, state)) =>
+        //println(s"$tid: not my turn but $i !")
+        false
+      case None =>
+        false
+    }
+  }
+
+  var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute.
+
+  /** returns true if the thread can continue to execute, and false otherwise */
+  def decide(): Option[(Int, ThreadState)] = {
+    if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision.
+      //println(s"$threadId: I'm taking a decision")
+      if (threadStates.values.forall { case e: Wait => true case _ => false }) {
+        val waiting = threadStates.keys.map(_.toString).mkString(", ")
+        val s = if (threadStates.size > 1) "s" else ""
+        val are = if (threadStates.size > 1) "are" else "is"
+        throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.")
+      } else {
+        // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode.
+        // Let's determine which ones can continue.
+        val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet
+        val threadsNotBlocked = threadStates.toSeq.filter {
+          case (id, v: VariableReadWrite)         => true
+          case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire)
+          case _                                  => false
+        }
+        if (threadsNotBlocked.isEmpty) {
+          val waiting = threadStates.keys.map(_.toString).mkString(", ")
+          val s = if (threadStates.size > 1) "s" else ""
+          val are = if (threadStates.size > 1) "are" else "is"
+          val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap
+          val reason = threadStates.collect {
+            case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) =>
+              s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}"
+          }.mkString("\n")
+          throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason")
+        } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute.
+          Some(threadsNotBlocked(0))
+        } else {
+          val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t })
+          if (next != -1) {
+            //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}")
+            val chosenOne = schedule(next) // TODO: Make schedule a mutable list.
+            schedule = schedule.take(next) ++ schedule.drop(next + 1)
+            Some((chosenOne, threadStates(chosenOne)))
+          } else {
+            threadPreference = (threadPreference + 1) % threadsNotBlocked.size
+            val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy
+            Some(chosenOne)
+            //threadsNotBlocked.indexOf(threadId) >= 0
+            /*
+            val tnb = threadsNotBlocked.map(_._1).mkString(",")
+            val s = if (schedule.isEmpty) "empty" else schedule.mkString(",")
+            val only = if (schedule.isEmpty) "" else " only"
+            throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/
+          }
+        }
+      }
+    } else canContinue
+  }
+
+  /**
+   * This will be called before a schedulable operation begins.
+   * This should not use synchronized
+   */
+  var numThreadsWaiting = new AtomicInteger(0)
+  //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice.
+  var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue.
+  private def waitForTurn = {
+    synchronized {
+      if (numThreadsWaiting.incrementAndGet() == threadStates.size) {
+        canContinue = decide()
+        notifyAll()
+      }
+      //waitingForDecision(threadId) = Some(numThreadsWaiting)
+      //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}")
+      while (!canProceed()) wait()
+    }
+    numThreadsWaiting.decrementAndGet()
+  }
+
+  /**
+   * To be invoked when a thread is about to complete
+   */
+  private def removeFromSchedule(fakeid: Int) = synchronized {
+    //println(s"$fakeid: I'm taking a decision because I finished")
+    schedule = schedule.filterNot(_ == fakeid)
+    threadStates -= fakeid
+    if (numThreadsWaiting.get() == threadStates.size) {
+      canContinue = decide()
+      notifyAll()
+    }
+  }
+
+  def getOperationLog() = opLog
+}
diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Stats.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Stats.scala
new file mode 100644
index 0000000000000000000000000000000000000000..bc1241c543227a71727d2ca2987e3bdc9fed3210
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/Stats.scala
@@ -0,0 +1,23 @@
+package m14
+package instrumentation
+
+import java.lang.management._
+
+/**
+ * A collection of methods that can be used to collect run-time statistics about Leon programs.
+ * This is mostly used to test the resources properties of Leon programs
+ */
+object Stats {
+  def timed[T](code: => T)(cont: Long => Unit): T = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    cont((System.currentTimeMillis() - t1))
+    r
+  }
+
+  def withTime[T](code: => T): (T, Long) = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    (r, (System.currentTimeMillis() - t1))
+  }
+}
diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestHelper.scala
new file mode 100644
index 0000000000000000000000000000000000000000..faa3505b2d85e546e20f9f228ca0ad1f6ac9c438
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestHelper.scala
@@ -0,0 +1,125 @@
+package m14
+package instrumentation
+
+import scala.util.Random
+import scala.collection.mutable.{Map => MutableMap}
+
+import Stats._
+
+object TestHelper {
+  val noOfSchedules = 10000 // set this to 100k during deployment
+  val readWritesPerThread = 20 // maximum number of read/writes possible in one thread
+  val contextSwitchBound = 10
+  val testTimeout = 240 // the total time out for a test in seconds
+  val schedTimeout = 15 // the total time out for execution of a schedule in secs
+
+  // Helpers
+  /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1))
+  def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/
+
+  def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) =
+    testManySchedules(1,
+      (sched: Scheduler) => {
+        (List(() => ops(sched)),
+         (res: List[Any]) => assertions(res.head.asInstanceOf[T]))
+      })
+
+  /**
+   * @numThreads number of threads
+   * @ops operations to be executed, one per thread
+   * @assertion as condition that will executed after all threads have completed (without exceptions)
+   * 					 the arguments are the results of the threads
+   */
+  def testManySchedules(numThreads: Int,
+      ops: Scheduler =>
+        (List[() => Any], // Threads
+         List[Any] => (Boolean, String)) // Assertion
+      ) = {
+    var timeout = testTimeout * 1000L
+    val threadIds = (1 to numThreads)
+    //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach {
+    val schedules = (new ScheduleGenerator(numThreads)).schedules()
+    var schedsExplored = 0
+    schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach {
+      //case _ if timeout <= 0 => // break
+      case schedule =>
+        schedsExplored += 1
+        val schedr = new Scheduler(schedule)
+        //println("Exploring Sched: "+schedule)
+        val (threadOps, assertion) = ops(schedr)
+        if (threadOps.size != numThreads)
+          throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps")
+        timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match {
+          case Timeout(msg) =>
+            throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg)
+          case Except(msg, stkTrace) =>
+            val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n")
+            throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr)
+          case RetVal(threadRes) =>
+            // check the assertion
+            val (success, custom_msg) = assertion(threadRes)
+            if (!success) {
+              val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n")
+              throw new java.lang.AssertionError("Assertion failed: "+msg)
+            }
+        }
+    }
+    if (timeout <= 0) {
+      throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!")
+    }
+  }
+
+  /**
+   * A schedule generator that is based on the context bound
+   */
+  class ScheduleGenerator(numThreads: Int) {
+    val scheduleLength = readWritesPerThread * numThreads
+    val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position
+    def schedules(): LazyList[List[Int]] = {
+      var contextSwitches = 0
+      var contexts = List[Int]() // a stack of thread ids in the order of context-switches
+      val remainingOps = MutableMap[Int, Int]()
+      remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread
+      val liveThreads = (1 to numThreads).toSeq.toBuffer
+
+      /**
+       * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule
+       */
+      def updateState(tid: Int): Unit = {
+        val remOps = remainingOps(tid)
+        if (remOps == 0) {
+          liveThreads -= tid
+        } else {
+          remainingOps += (tid -> (remOps - 1))
+        }
+      }
+      val schedule = rands.foldLeft(List[Int]()) {
+        case (acc, r) if contextSwitches < contextSwitchBound =>
+          val tid = liveThreads(r.nextInt(liveThreads.size))
+          contexts match {
+            case prev :: tail if prev != tid => // we have a new context switch here
+              contexts +:= tid
+              contextSwitches += 1
+            case prev :: tail =>
+            case _ => // init case
+              contexts +:= tid
+          }
+          updateState(tid)
+          acc :+ tid
+        case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches
+          if (!contexts.isEmpty) {
+            contexts = contexts.dropWhile(remainingOps(_) == 0)
+          }
+          val tid = contexts match {
+            case top :: tail => top
+            case _ => liveThreads(0)  // here, there has to be threads that have not even started
+          }
+          updateState(tid)
+          acc :+ tid
+      }
+      schedule #:: schedules()
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestUtils.scala
new file mode 100644
index 0000000000000000000000000000000000000000..f980f99e34d653acde5590cb5e1e508607d3b61b
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/test/scala/m14/instrumentation/TestUtils.scala
@@ -0,0 +1,20 @@
+package m14
+package instrumentation
+
+import scala.concurrent._
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object TestUtils {
+  def failsOrTimesOut[T](action: => T): Boolean = {
+    val asyncAction = Future {
+      action
+    }
+    try {
+      Await.result(asyncAction, 2000.millisecond)
+    } catch {
+      case _: Throwable => return true
+    }
+    return false
+  }
+}
diff --git a/previous-exams/2021-midterm-solutions/m15.md b/previous-exams/2021-midterm/m15.md
similarity index 95%
rename from previous-exams/2021-midterm-solutions/m15.md
rename to previous-exams/2021-midterm/m15.md
index 781e05a4681b94b94c4f8376bec6c2fae7d592cb..dd450975e1d5b2ccebf15375e61c87c9a4519437 100644
--- a/previous-exams/2021-midterm-solutions/m15.md
+++ b/previous-exams/2021-midterm/m15.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m15 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m15
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m15/.gitignore b/previous-exams/2021-midterm/m15/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m15/assignment.sbt b/previous-exams/2021-midterm/m15/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m15/build.sbt b/previous-exams/2021-midterm/m15/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..3b7539dcef795f03ecd7ed4ffcb5eb6839054b21
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m15"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m15.M15Suite"
diff --git a/previous-exams/2021-midterm/m15/grading-tests.jar b/previous-exams/2021-midterm/m15/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..be10881c427c928b9cb66f040d9bc864841c1499
Binary files /dev/null and b/previous-exams/2021-midterm/m15/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m15/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m15/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m15/project/MOOCSettings.scala b/previous-exams/2021-midterm/m15/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m15/project/StudentTasks.scala b/previous-exams/2021-midterm/m15/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m15/project/build.properties b/previous-exams/2021-midterm/m15/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m15/project/buildSettings.sbt b/previous-exams/2021-midterm/m15/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m15/project/plugins.sbt b/previous-exams/2021-midterm/m15/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractBlockingQueue.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractBlockingQueue.scala
new file mode 100644
index 0000000000000000000000000000000000000000..85a28b26c1d28327725d98d501e4f855dce0f25e
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractBlockingQueue.scala
@@ -0,0 +1,14 @@
+package m15
+
+abstract class AbstractBlockingQueue[T] extends Monitor {
+  private var underlying: List[T] = Nil
+
+  def getUnderlying(): List[T] =
+    underlying
+
+  def setUnderlying(newValue: List[T]): Unit =
+    underlying = newValue
+
+  def put(elem: T): Unit
+  def take(): T
+}
diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractThreadPoolExecutor.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractThreadPoolExecutor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..5e663e8b70cd7482feca29b9791bcd766174ae49
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/AbstractThreadPoolExecutor.scala
@@ -0,0 +1,7 @@
+package m15
+
+abstract class AbstractThreadPoolExecutor {
+  def execute(task: Unit => Unit): Unit
+  def start(): Unit
+  def shutdown(): Unit
+}
diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/M15.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/M15.scala
new file mode 100644
index 0000000000000000000000000000000000000000..00a4aed86504e6578a3dd70fa37bfbff7cc4bbcb
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/M15.scala
@@ -0,0 +1,65 @@
+package m15
+
+object M15 {
+  /** A thread pool that executes submitted task using one of several threads */
+  class ThreadPoolExecutor(taskQueue: BlockingQueue[Unit => Unit], poolSize: Int)
+      extends AbstractThreadPoolExecutor {
+
+    private class Worker extends Thread {
+      override def run(): Unit = {
+        try {
+          while (true) {
+            ???
+          }
+        } catch {
+          case e: InterruptedException =>
+            // Nothing to do here, we are shutting down gracefully.
+        }
+      }
+    }
+    private val workers: List[Worker] = List.fill(poolSize)(new Worker())
+
+    /** Executes the given task, passed by name. */
+    def execute(task: Unit => Unit): Unit =
+      ???
+
+    /** Starts the thread pool. */
+    def start(): Unit =
+      workers.foreach(_.start())
+
+    /** Instantly shuts down all actively executing tasks using an interrupt. */
+    def shutdown(): Unit =
+      workers.foreach(_.interrupt())
+  }
+
+  /**
+   * A queue whose take operations blocks until the queue become non-empty.
+   * Elements must be retrived from this queue in a first in, first out order.
+   * All methods of this class are thread safe, that is, they can safely
+   * be used from multiple thread without any particular synchronization.
+   */
+  class BlockingQueue[T] extends AbstractBlockingQueue[T] {
+
+    // The state of this queue is stored in an underlying List[T] defined in
+    // the AbstractBlockingQueue class. Your implementation should access and
+    // update this list using the following setter and getter methods:
+    // - def getUnderlying(): List[T]
+    // - def setUnderlying(newValue: List[T]): Unit
+    // Using these methods is required for testing purposes.
+
+    /** Inserts the specified element into this queue (non-blocking) */
+    def put(elem: T): Unit =
+      ???
+
+    /**
+     * Retrieves and removes the head of this queue, waiting if necessary
+     * until an element becomes available (blocking).
+     * This queue operates in a first in, first out order.
+     */
+    def take(): T =
+      // Hint: The .last/.init methods on List are dual of .head/.head,
+      // they can be used to retrive the last element and the initial part of
+      // the list without its last element.
+      ???
+  }
+}
diff --git a/previous-exams/2021-midterm/m15/src/main/scala/m15/Monitor.scala b/previous-exams/2021-midterm/m15/src/main/scala/m15/Monitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..b64e697d613b2d44c3f892e4ae0eebf028b5d5e8
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/Monitor.scala
@@ -0,0 +1,23 @@
+package m15
+
+class Dummy
+
+trait Monitor {
+  implicit val dummy: Dummy = new Dummy
+
+  def wait()(implicit i: Dummy) = waitDefault()
+
+  def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e)
+
+  def notify()(implicit i: Dummy) = notifyDefault()
+
+  def notifyAll()(implicit i: Dummy) = notifyAllDefault()
+
+  private val lock = new AnyRef
+
+  // Can be overriden.
+  def waitDefault(): Unit = lock.wait()
+  def synchronizedDefault[T](toExecute: => T): T = lock.synchronized(toExecute)
+  def notifyDefault(): Unit = lock.notify()
+  def notifyAllDefault(): Unit = lock.notifyAll()
+}
diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/M15Suite.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/M15Suite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..e0f2243993c4f40b8e43b0b51e1e45b8c77d2c21
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/M15Suite.scala
@@ -0,0 +1,281 @@
+package m15
+
+import instrumentation.SchedulableBlockingQueue
+import instrumentation.TestHelper._
+import instrumentation.TestUtils._
+
+class M15Suite extends munit.FunSuite {
+  import M15._
+
+  test("ThreadPool should put jobs in the queue, Workers should execute jobs from the queue (10pts)") {
+    case class PutE(e: Unit => Unit) extends Exception
+    val nThreads = 3
+    var taken = false
+    class TestBlockingQueue extends BlockingQueue[Unit => Unit] {
+      override def put(e: Unit => Unit): Unit =
+        throw new PutE(e)
+
+      override def take(): Unit => Unit =
+        x => {
+          taken = true
+          Thread.sleep(10 * 1000)
+        }
+    }
+
+    val tpe = new ThreadPoolExecutor(new TestBlockingQueue, nThreads)
+    val unit2unit: Unit => Unit = x => ()
+    try {
+      tpe.execute(unit2unit)
+      assert(false, "ThreadPoolExecutor does not put jobs in the queue")
+    } catch {
+      case PutE(e) =>
+        assert(e == unit2unit)
+    }
+    tpe.start()
+    Thread.sleep(1000)
+    assert(taken, s"ThreadPoolExecutor workers do no execute jobs from the queue")
+    tpe.shutdown()
+  }
+
+  test("BlockingQueue should work in a sequential setting (1pts)") {
+    testSequential[(Int, Int, Int, Int)]{ sched =>
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      queue.put(1)
+      queue.put(2)
+      queue.put(3)
+      queue.put(4)
+      (queue.take(),
+      queue.take(),
+      queue.take(),
+      queue.take())
+    }{ tuple =>
+      (tuple == (1, 2, 3, 4), s"Expected (1, 2, 3, 4) got $tuple")
+    }
+  }
+
+  test("BlockingQueue should work when Thread 1: 'put(1)', Thread 2: 'take' (3pts)") {
+    testManySchedules(2, sched => {
+      val queue = new SchedulableBlockingQueue[Int](sched)
+      (List(() => queue.put(1), () => queue.take()),
+       args => (args(1) == 1, s"Expected 1, got ${args(1)}"))
+    })
+  }
+
+  test("BlockingQueue should not be able to take from an empty queue (3pts)") {
+    testSequential[Boolean]{ sched =>
+      val queue = new SchedulableBlockingQueue[Int](sched);
+      queue.put(1)
+      queue.put(2)
+      queue.take()
+      queue.take()
+      failsOrTimesOut(queue.take())
+    }{ res =>
+      (res, "Was able to retrieve an element from an empty queue")
+    }
+  }
+
+  test("Should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take', and a buffer of size 1") {
+    testManySchedules(3, sched => {
+      val prodCons = new SchedulableBlockingQueue[Int](sched)
+      (List(() => prodCons.put(1), () => prodCons.put(2), () => prodCons.take())
+      , args => {
+        val takeRes = args(2).asInstanceOf[Int]
+        val nocreation = (takeRes == 1 || takeRes == 2)
+        if (!nocreation)
+          (false, s"'take' should return either 1 or 2")
+        else (true, "")
+      })
+    })
+  }
+
+  // testing no duplication
+  test("Should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'take', Thread 4: 'take', and a buffer of size 3") {
+    testManySchedules(4, sched => {
+      val prodCons = new SchedulableBlockingQueue[Int](sched)
+      (List(() => prodCons.put(1), () => prodCons.put(2), () => prodCons.take(), () => prodCons.take())
+      , args => {
+        def m(): (Boolean, String) = {
+          val takeRes1 = args(2).asInstanceOf[Int]
+          val takeRes2 = args(3).asInstanceOf[Int]
+          val nocreation = (x: Int) => List(1, 2).contains(x)
+          if (!nocreation(takeRes1))
+            return (false, s"'Thread 3: take' returned $takeRes1 but should return a value in {1, 2, 3}")
+          if (!nocreation(takeRes2))
+            return (false, s"'Thread 4: take' returned $takeRes2 but should return a value in {1, 2, 3}")
+
+          val noduplication = takeRes1 != takeRes2
+          if (!noduplication)
+            (false, s"'Thread 3 and 4' returned the same value: $takeRes1")
+          else (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  // testing no duplication with 5 threads
+  test("Should work when Thread 1: 'put(1)', Thread 2: 'put(2)', Thread 3: 'put(3)', Thread 4: 'take', Thread 5: 'take', and a buffer of size 1") {
+    testManySchedules(5, sched => {
+      val prodCons = new SchedulableBlockingQueue[Int](sched)
+      (List(() => prodCons.put(1), () => prodCons.put(2), () => prodCons.put(3),
+        () => prodCons.take(), () => prodCons.take())
+      , args => {
+        def m(): (Boolean, String) = {
+          val takeRes1 = args(3).asInstanceOf[Int]
+          val takeRes2 = args(4).asInstanceOf[Int]
+          val nocreation = (x: Int) => List(1, 2, 3).contains(x)
+          if (!nocreation(takeRes1))
+            return (false, s"'Thread 4: take' returned $takeRes1 but should return a value in {1, 2, 3}")
+          if (!nocreation(takeRes2))
+            return (false, s"'Thread 5: take' returned $takeRes2 but should return a value in {1, 2, 3}")
+
+          val noduplication = takeRes1 != takeRes2
+          if (!noduplication)
+            return (false, s"'Thread 4 and 5' returned the same value: $takeRes1")
+          else (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  // testing fifo buffer size 1
+  test("Should work when Thread 1: 'put(1); put(2)', Thread 2: 'take', Thread 3: 'put(3)', Thread 4: 'put(4)', and a buffer of size 3") {
+    testManySchedules(4, sched => {
+      val prodCons = new SchedulableBlockingQueue[Int](sched)
+      (List(() => { prodCons.put(1); prodCons.put(2) }, () => prodCons.take(),
+        () => prodCons.put(3), () => prodCons.put(4))
+      , args => {
+        def m(): (Boolean, String) = {
+          val takeRes = args(1).asInstanceOf[Int]
+          // no creation
+          val nocreation = (x: Int) => List(1, 2, 3, 4).contains(x)
+          if (!nocreation(takeRes))
+            return (false, s"'Thread 2: take' returned $takeRes, but should return a value in {1, 2, 3, 4}")
+          // fifo (cannot have 2 without 1)
+          if (takeRes == 2)
+            (false, s"'Thread 2' returned 2 before returning 1")
+          else
+            (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  // testing fifo buffer size 5
+  test("Should work when Thread 1: 'put(1); put(2)', Thread 2: 'take', Thread 3: 'put(11)', Thread 4: 'put(10)', and a buffer of size 5") {
+    testManySchedules(4, sched => {
+      val prodCons = new SchedulableBlockingQueue[Int](sched)
+      (List(() => { prodCons.put(1); prodCons.put(2) }, () => prodCons.take(),
+        () => prodCons.put(11), () => prodCons.put(10))
+      , args => {
+        def m(): (Boolean, String) = {
+          val takeRes = args(1).asInstanceOf[Int]
+          // no creation
+          val nocreation = (x: Int) => List(1, 2, 10, 11).contains(x)
+          if (!nocreation(takeRes))
+            return (false, s"'Thread 2: take' returned $takeRes, but should return a value in {1, 2, 10, 11}")
+          // fifo (cannot have 2 without 1)
+          if (takeRes == 2)
+            (false, s"'Thread 2' returned 2 before returning 1")
+          else
+            (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  // testing fifo on more complicated case
+  test("Should work when Thread 1: 'put(1); put(3)', Thread 2: 'put(2)', Thread 3: 'put(4)', Thread 4: 'take', Thread 5: 'take', and a buffer of size 10") {
+    testManySchedules(5, sched => {
+      val prodCons = new SchedulableBlockingQueue[Int](sched)
+      (List(() => { prodCons.put(1); prodCons.put(3) }, () => prodCons.put(2),
+        () => prodCons.put(4), () => prodCons.take(), () => prodCons.take())
+      , args => {
+        def m(): (Boolean, String) = {
+          val takeRes1 = args(3).asInstanceOf[Int]
+          val takeRes2 = args(4).asInstanceOf[Int]
+          // no creation
+          val nocreation = (x: Int) => List(1, 2, 3, 4).contains(x)
+          if (!nocreation(takeRes1))
+            return (false, s"'Thread 4: take' returned $takeRes1 but should return a value in {1, 2, 3, 4}")
+          if (!nocreation(takeRes2))
+            return (false, s"'Thread 5: take' returned $takeRes2 but should return a value in {1, 2, 3, 4}")
+          // no duplication
+          if (takeRes1 == takeRes2)
+            return (false, s"'Thread 4 and 5' returned the same value: $takeRes1")
+          // fifo (cannot have 3 without 1)
+          val takes = List(takeRes1, takeRes2)
+          if (takes.contains(3) && !takes.contains(1))
+            (false, s"'Thread 4 or 5' returned 3 before returning 1")
+          else
+            (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  // combining put and take in one thread
+  test("Should work when Thread 1: 'put(21); put(22)', Thread 2: 'take', Thread 3: 'put(23); take', Thread 4: 'put(24); take', and a buffer of size 2") {
+    testManySchedules(4, sched => {
+      val prodCons = new SchedulableBlockingQueue[Int](sched)
+      (List(() => { prodCons.put(21); prodCons.put(22) }, () => prodCons.take(),
+        () => { prodCons.put(23); prodCons.take() }, () => { prodCons.put(24); prodCons.take() })
+      , args => {
+        def m(): (Boolean, String) = {
+          val takes = List(args(1).asInstanceOf[Int], args(2).asInstanceOf[Int], args(3).asInstanceOf[Int])
+          // no creation
+          val vals = List(21, 22, 23, 24)
+
+          var i = 0
+          while (i < takes.length) {
+            val x = takes(i)
+            if (!vals.contains(x))
+              return (false, s"'Thread $i: take' returned $x but should return a value in $vals")
+            i += 1
+          }
+
+          // no duplication
+          if (takes.distinct.size != takes.size)
+            return (false, s"Takes did not return unique values: $takes")
+          // fifo (cannot have 22 without 21)
+          if (takes.contains(22) && !takes.contains(21))
+            (false, s"`Takes returned 22 before returning 21")
+          else
+            (true, "")
+        }
+        m()
+      })
+    })
+  }
+
+  // completely hidden hard to crack test
+  test("[Black box test] Values should be taken in the order they are put") {
+    testManySchedules(4, sched => {
+      val prodCons = new SchedulableBlockingQueue[(Char, Int)](sched)
+      val n = 2
+      (List(
+        () => for (i <- 1 to n) { prodCons.put(('a', i)) },
+        () => for (i <- 1 to n) { prodCons.put(('b', i)) },
+        () => for (i <- 1 to n) { prodCons.put(('c', i)) },
+        () => {
+          import scala.collection.mutable
+          var counts = mutable.HashMap.empty[Char, Int]
+          counts('a') = 0
+          counts('b') = 0
+          counts('c') = 0
+          for (i <- 1 to (3 * n)) {
+            val (c, n) = prodCons.take()
+            counts(c) += 1
+            assert(counts(c) == n)
+          }
+        })
+      , _ =>
+        (true, "")
+      )
+    })
+  }
+}
diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/MockedMonitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c0591e3e03adc249e4a857600d20362bed219ba9
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/MockedMonitor.scala
@@ -0,0 +1,73 @@
+package m15
+package instrumentation
+
+trait MockedMonitor extends Monitor {
+  def scheduler: Scheduler
+
+  // Can be overriden.
+  override def waitDefault() = {
+    scheduler.log("wait")
+    scheduler updateThreadState Wait(this, scheduler.threadLocks.tail)
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    scheduler.log("synchronized check")
+    val prevLocks = scheduler.threadLocks
+    scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue.
+    scheduler.log("synchronized -> enter")
+    try {
+      toExecute
+    } finally {
+      scheduler updateThreadState Running(prevLocks)
+      scheduler.log("synchronized -> out")
+    }
+  }
+  override def notifyDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notify")
+  }
+  override def notifyAllDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notifyAll")
+  }
+}
+
+trait LockFreeMonitor extends Monitor {
+  override def waitDefault() = {
+    throw new Exception("Please use lock-free structures and do not use wait()")
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    throw new Exception("Please use lock-free structures and do not use synchronized()")
+  }
+  override def notifyDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notify()")
+  }
+  override def notifyAllDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notifyAll()")
+  }
+}
+
+
+abstract class ThreadState {
+  def locks: Seq[AnyRef]
+}
+trait CanContinueIfAcquiresLock extends ThreadState {
+  def lockToAquire: AnyRef
+}
+case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState
+case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Running(locks: Seq[AnyRef]) extends ThreadState
+case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState
diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/SchedulableBlockingQueue.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/SchedulableBlockingQueue.scala
new file mode 100644
index 0000000000000000000000000000000000000000..31b09bef249bc17111043612f069402ef13bdf4f
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/SchedulableBlockingQueue.scala
@@ -0,0 +1,17 @@
+package m15
+package instrumentation
+
+class SchedulableBlockingQueue[T](val scheduler: Scheduler)
+  extends m15.M15.BlockingQueue[T] with MockedMonitor {
+    private var underlying: List[T] = Nil
+
+    override def getUnderlying(): List[T] =
+      scheduler.exec {
+        underlying
+      }(s"Get $underlying")
+
+    override def setUnderlying(newValue: List[T]): Unit =
+      scheduler.exec {
+        underlying = newValue
+      }(s"Set $newValue")
+}
diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Scheduler.scala
new file mode 100644
index 0000000000000000000000000000000000000000..fd5f427bb86376709efeebebf0dbabc1bc96e70a
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Scheduler.scala
@@ -0,0 +1,305 @@
+package m15
+package instrumentation
+
+import java.util.concurrent._;
+import scala.concurrent.duration._
+import scala.collection.mutable._
+import Stats._
+
+import java.util.concurrent.atomic.AtomicInteger
+
+sealed abstract class Result
+case class RetVal(rets: List[Any]) extends Result
+case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result
+case class Timeout(msg: String) extends Result
+
+/**
+ * A class that maintains schedule and a set of thread ids.
+ * The schedules are advanced after an operation of a SchedulableBuffer is performed.
+ * Note: the real schedule that is executed may deviate from the input schedule
+ * due to the adjustments that had to be made for locks
+ */
+class Scheduler(sched: List[Int]) {
+  val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform
+
+  private var schedule = sched
+  private var numThreads = 0
+  private val realToFakeThreadId = Map[Long, Int]()
+  private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat)
+  private val threadStates = Map[Int, ThreadState]()
+
+  /**
+   * Runs a set of operations in parallel as per the schedule.
+   * Each operation may consist of many primitive operations like reads or writes
+   * to shared data structure each of which should be executed using the function `exec`.
+   * @timeout in milliseconds
+   * @return true - all threads completed on time,  false -some tests timed out.
+   */
+  def runInParallel(timeout: Long, ops: List[() => Any]): Result = {
+    numThreads = ops.length
+    val threadRes = Array.fill(numThreads) { None: Any }
+    var exception: Option[Except] = None
+    val syncObject = new Object()
+    var completed = new AtomicInteger(0)
+    // create threads
+    val threads = ops.zipWithIndex.map {
+      case (op, i) =>
+        new Thread(new Runnable() {
+          def run(): Unit = {
+            val fakeId = i + 1
+            setThreadId(fakeId)
+            try {
+              updateThreadState(Start)
+              val res = op()
+              updateThreadState(End)
+              threadRes(i) = res
+              // notify the master thread if all threads have completed
+              if (completed.incrementAndGet() == ops.length) {
+                syncObject.synchronized { syncObject.notifyAll() }
+              }
+            } catch {
+              case e: Throwable if exception != None => // do nothing here and silently fail
+              case e: Throwable =>
+                log(s"throw ${e.toString}")
+                exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"),
+                    e.getStackTrace))
+                syncObject.synchronized { syncObject.notifyAll() }
+              //println(s"$fakeId: ${e.toString}")
+              //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads)
+            }
+          }
+        })
+    }
+    // start all threads
+    threads.foreach(_.start())
+    // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire
+    var remTime = timeout
+    syncObject.synchronized {
+      timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time }
+    }
+    if (exception.isDefined) {
+      exception.get
+    } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors
+      Timeout(opLog.mkString("\n"))
+    } else {
+      // every thing executed normally
+      RetVal(threadRes.toList)
+    }
+  }
+
+  // Updates the state of the current thread
+  def updateThreadState(state: ThreadState): Unit = {
+    val tid = threadId
+    synchronized {
+      threadStates(tid) = state
+    }
+    state match {
+      case Sync(lockToAquire, locks) =>
+        if (locks.indexOf(lockToAquire) < 0) waitForTurn else {
+          // Re-aqcuiring the same lock
+          updateThreadState(Running(lockToAquire +: locks))
+        }
+      case Start      => waitStart()
+      case End        => removeFromSchedule(tid)
+      case Running(_) =>
+      case _          => waitForTurn // Wait, SyncUnique, VariableReadWrite
+    }
+  }
+
+  def waitStart(): Unit = {
+    //while (threadStates.size < numThreads) {
+    //Thread.sleep(1)
+    //}
+    synchronized {
+      if (threadStates.size < numThreads) {
+        wait()
+      } else {
+        notifyAll()
+      }
+    }
+  }
+
+  def threadLocks = {
+    synchronized {
+      threadStates(threadId).locks
+    }
+  }
+
+  def threadState = {
+    synchronized {
+      threadStates(threadId)
+    }
+  }
+
+  def mapOtherStates(f: ThreadState => ThreadState) = {
+    val exception = threadId
+    synchronized {
+      for (k <- threadStates.keys if k != exception) {
+        threadStates(k) = f(threadStates(k))
+      }
+    }
+  }
+
+  def log(str: String) = {
+    if((realToFakeThreadId contains Thread.currentThread().getId())) {
+      val space = (" " * ((threadId - 1) * 2))
+      val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + "  ")
+      opLog += s
+    }
+  }
+
+  /**
+   * Executes a read or write operation to a global data structure as per the given schedule
+   * @param msg a message corresponding to the operation that will be logged
+   */
+  def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = {
+    if(! (realToFakeThreadId contains Thread.currentThread().getId())) {
+      primop
+    } else {
+      updateThreadState(VariableReadWrite(threadLocks))
+      val m = msg
+      if(m != "") log(m)
+      if (opLog.size > maxOps)
+        throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!")
+      val res = primop
+      postMsg match {
+        case Some(m) => log(m(res))
+        case None =>
+      }
+      res
+    }
+  }
+
+  private def setThreadId(fakeId: Int) = synchronized {
+    realToFakeThreadId(Thread.currentThread.getId) = fakeId
+  }
+
+  def threadId =
+    try {
+      realToFakeThreadId(Thread.currentThread().getId())
+    } catch {
+    case e: NoSuchElementException =>
+      throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!")
+    }
+
+  private def isTurn(tid: Int) = synchronized {
+    (!schedule.isEmpty && schedule.head != tid)
+  }
+
+  def canProceed(): Boolean = {
+    val tid = threadId
+    canContinue match {
+      case Some((i, state)) if i == tid =>
+        //println(s"$tid: Runs ! Was in state $state")
+        canContinue = None
+        state match {
+          case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks))
+          case SyncUnique(lockToAquire, locks) =>
+            mapOtherStates {
+              _ match {
+                case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2)
+                case e => e
+              }
+            }
+            updateThreadState(Running(lockToAquire +: locks))
+          case VariableReadWrite(locks) => updateThreadState(Running(locks))
+        }
+        true
+      case Some((i, state)) =>
+        //println(s"$tid: not my turn but $i !")
+        false
+      case None =>
+        false
+    }
+  }
+
+  var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute.
+
+  /** returns true if the thread can continue to execute, and false otherwise */
+  def decide(): Option[(Int, ThreadState)] = {
+    if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision.
+      //println(s"$threadId: I'm taking a decision")
+      if (threadStates.values.forall { case e: Wait => true case _ => false }) {
+        val waiting = threadStates.keys.map(_.toString).mkString(", ")
+        val s = if (threadStates.size > 1) "s" else ""
+        val are = if (threadStates.size > 1) "are" else "is"
+        throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.")
+      } else {
+        // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode.
+        // Let's determine which ones can continue.
+        val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet
+        val threadsNotBlocked = threadStates.toSeq.filter {
+          case (id, v: VariableReadWrite)         => true
+          case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire)
+          case _                                  => false
+        }
+        if (threadsNotBlocked.isEmpty) {
+          val waiting = threadStates.keys.map(_.toString).mkString(", ")
+          val s = if (threadStates.size > 1) "s" else ""
+          val are = if (threadStates.size > 1) "are" else "is"
+          val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap
+          val reason = threadStates.collect {
+            case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) =>
+              s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}"
+          }.mkString("\n")
+          throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason")
+        } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute.
+          Some(threadsNotBlocked(0))
+        } else {
+          val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t })
+          if (next != -1) {
+            //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}")
+            val chosenOne = schedule(next) // TODO: Make schedule a mutable list.
+            schedule = schedule.take(next) ++ schedule.drop(next + 1)
+            Some((chosenOne, threadStates(chosenOne)))
+          } else {
+            threadPreference = (threadPreference + 1) % threadsNotBlocked.size
+            val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy
+            Some(chosenOne)
+            //threadsNotBlocked.indexOf(threadId) >= 0
+            /*
+            val tnb = threadsNotBlocked.map(_._1).mkString(",")
+            val s = if (schedule.isEmpty) "empty" else schedule.mkString(",")
+            val only = if (schedule.isEmpty) "" else " only"
+            throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/
+          }
+        }
+      }
+    } else canContinue
+  }
+
+  /**
+   * This will be called before a schedulable operation begins.
+   * This should not use synchronized
+   */
+  var numThreadsWaiting = new AtomicInteger(0)
+  //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice.
+  var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue.
+  private def waitForTurn = {
+    synchronized {
+      if (numThreadsWaiting.incrementAndGet() == threadStates.size) {
+        canContinue = decide()
+        notifyAll()
+      }
+      //waitingForDecision(threadId) = Some(numThreadsWaiting)
+      //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}")
+      while (!canProceed()) wait()
+    }
+    numThreadsWaiting.decrementAndGet()
+  }
+
+  /**
+   * To be invoked when a thread is about to complete
+   */
+  private def removeFromSchedule(fakeid: Int) = synchronized {
+    //println(s"$fakeid: I'm taking a decision because I finished")
+    schedule = schedule.filterNot(_ == fakeid)
+    threadStates -= fakeid
+    if (numThreadsWaiting.get() == threadStates.size) {
+      canContinue = decide()
+      notifyAll()
+    }
+  }
+
+  def getOperationLog() = opLog
+}
diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Stats.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Stats.scala
new file mode 100644
index 0000000000000000000000000000000000000000..e82c09813f44c824cdfa55d54d8dd00acb584b48
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/Stats.scala
@@ -0,0 +1,23 @@
+package m15
+package instrumentation
+
+import java.lang.management._
+
+/**
+ * A collection of methods that can be used to collect run-time statistics about Leon programs.
+ * This is mostly used to test the resources properties of Leon programs
+ */
+object Stats {
+  def timed[T](code: => T)(cont: Long => Unit): T = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    cont((System.currentTimeMillis() - t1))
+    r
+  }
+
+  def withTime[T](code: => T): (T, Long) = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    (r, (System.currentTimeMillis() - t1))
+  }
+}
diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestHelper.scala
new file mode 100644
index 0000000000000000000000000000000000000000..5f863382c87cb808f70a3ed4aacabec1496f15c4
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestHelper.scala
@@ -0,0 +1,125 @@
+package m15
+package instrumentation
+
+import scala.util.Random
+import scala.collection.mutable.{Map => MutableMap}
+
+import Stats._
+
+object TestHelper {
+  val noOfSchedules = 10000 // set this to 100k during deployment
+  val readWritesPerThread = 20 // maximum number of read/writes possible in one thread
+  val contextSwitchBound = 10
+  val testTimeout = 240 // the total time out for a test in seconds
+  val schedTimeout = 15 // the total time out for execution of a schedule in secs
+
+  // Helpers
+  /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1))
+  def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/
+
+  def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) =
+    testManySchedules(1,
+      (sched: Scheduler) => {
+        (List(() => ops(sched)),
+         (res: List[Any]) => assertions(res.head.asInstanceOf[T]))
+      })
+
+  /**
+   * @numThreads number of threads
+   * @ops operations to be executed, one per thread
+   * @assertion as condition that will executed after all threads have completed (without exceptions)
+   * 					 the arguments are the results of the threads
+   */
+  def testManySchedules(numThreads: Int,
+      ops: Scheduler =>
+        (List[() => Any], // Threads
+         List[Any] => (Boolean, String)) // Assertion
+      ) = {
+    var timeout = testTimeout * 1000L
+    val threadIds = (1 to numThreads)
+    //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach {
+    val schedules = (new ScheduleGenerator(numThreads)).schedules()
+    var schedsExplored = 0
+    schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach {
+      //case _ if timeout <= 0 => // break
+      case schedule =>
+        schedsExplored += 1
+        val schedr = new Scheduler(schedule)
+        //println("Exploring Sched: "+schedule)
+        val (threadOps, assertion) = ops(schedr)
+        if (threadOps.size != numThreads)
+          throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps")
+        timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match {
+          case Timeout(msg) =>
+            throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg)
+          case Except(msg, stkTrace) =>
+            val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n")
+            throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr)
+          case RetVal(threadRes) =>
+            // check the assertion
+            val (success, custom_msg) = assertion(threadRes)
+            if (!success) {
+              val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n")
+              throw new java.lang.AssertionError("Assertion failed: "+msg)
+            }
+        }
+    }
+    if (timeout <= 0) {
+      throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!")
+    }
+  }
+
+  /**
+   * A schedule generator that is based on the context bound
+   */
+  class ScheduleGenerator(numThreads: Int) {
+    val scheduleLength = readWritesPerThread * numThreads
+    val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position
+    def schedules(): LazyList[List[Int]] = {
+      var contextSwitches = 0
+      var contexts = List[Int]() // a stack of thread ids in the order of context-switches
+      val remainingOps = MutableMap[Int, Int]()
+      remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread
+      val liveThreads = (1 to numThreads).toSeq.toBuffer
+
+      /**
+       * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule
+       */
+      def updateState(tid: Int): Unit = {
+        val remOps = remainingOps(tid)
+        if (remOps == 0) {
+          liveThreads -= tid
+        } else {
+          remainingOps += (tid -> (remOps - 1))
+        }
+      }
+      val schedule = rands.foldLeft(List[Int]()) {
+        case (acc, r) if contextSwitches < contextSwitchBound =>
+          val tid = liveThreads(r.nextInt(liveThreads.size))
+          contexts match {
+            case prev :: tail if prev != tid => // we have a new context switch here
+              contexts +:= tid
+              contextSwitches += 1
+            case prev :: tail =>
+            case _ => // init case
+              contexts +:= tid
+          }
+          updateState(tid)
+          acc :+ tid
+        case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches
+          if (!contexts.isEmpty) {
+            contexts = contexts.dropWhile(remainingOps(_) == 0)
+          }
+          val tid = contexts match {
+            case top :: tail => top
+            case _ => liveThreads(0)  // here, there has to be threads that have not even started
+          }
+          updateState(tid)
+          acc :+ tid
+      }
+      schedule #:: schedules()
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestUtils.scala
new file mode 100644
index 0000000000000000000000000000000000000000..3f4afe8845bff9e2608f3b2e3b92a8739ad238cc
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/test/scala/m15/instrumentation/TestUtils.scala
@@ -0,0 +1,20 @@
+package m15
+package instrumentation
+
+import scala.concurrent._
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object TestUtils {
+  def failsOrTimesOut[T](action: => T): Boolean = {
+    val asyncAction = Future {
+      action
+    }
+    try {
+      Await.result(asyncAction, 2000.millisecond)
+    } catch {
+      case _: Throwable => return true
+    }
+    return false
+  }
+}
diff --git a/previous-exams/2021-midterm-solutions/m2.md b/previous-exams/2021-midterm/m2.md
similarity index 95%
rename from previous-exams/2021-midterm-solutions/m2.md
rename to previous-exams/2021-midterm/m2.md
index 47b097671c167b9e5f677e622ac8ce3572b09a09..fb3fdb6cdbf1b3aa21b4220d71f966caaec9b9bf 100644
--- a/previous-exams/2021-midterm-solutions/m2.md
+++ b/previous-exams/2021-midterm/m2.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m2 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m2
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m2/.gitignore b/previous-exams/2021-midterm/m2/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m2/assignment.sbt b/previous-exams/2021-midterm/m2/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m2/build.sbt b/previous-exams/2021-midterm/m2/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..4a68d9e22fc13fac03309e5751f98bf7dd08349e
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m2"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m2.M2Suite"
diff --git a/previous-exams/2021-midterm/m2/grading-tests.jar b/previous-exams/2021-midterm/m2/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..0378ba0b46a3d1a19bbb73a2dbdf4c9b77b8cb83
Binary files /dev/null and b/previous-exams/2021-midterm/m2/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m2/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m2/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m2/project/MOOCSettings.scala b/previous-exams/2021-midterm/m2/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m2/project/StudentTasks.scala b/previous-exams/2021-midterm/m2/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m2/project/build.properties b/previous-exams/2021-midterm/m2/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m2/project/buildSettings.sbt b/previous-exams/2021-midterm/m2/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m2/project/plugins.sbt b/previous-exams/2021-midterm/m2/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m2/src/main/scala/m2/Lib.scala b/previous-exams/2021-midterm/m2/src/main/scala/m2/Lib.scala
new file mode 100644
index 0000000000000000000000000000000000000000..9f1aff59d87f86ee2092ec49479f388b4054a643
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/src/main/scala/m2/Lib.scala
@@ -0,0 +1,60 @@
+package m2
+
+////////////////////////////////////////
+// NO NEED TO MODIFY THIS SOURCE FILE //
+////////////////////////////////////////
+
+trait Lib {
+
+  /** If an array has `n` elements and `n < THRESHOLD`, then it should be processed sequentially */
+  final val THRESHOLD: Int = 33
+
+  /** Compute the two values in parallel
+   *
+   *  Note: Most tests just compute those two sequentially to make any bug simpler to debug
+   */
+  def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2)
+
+  /** A limited array. It only contains the required operations for this exercise. */
+  trait Arr[T] {
+    /** Get the i-th element of the array (0-based) */
+    def apply(i: Int): T
+    /** Update the i-th element of the array with the given value (0-based) */
+    def update(i: Int, x: T): Unit
+    /** Number of elements in this array */
+    def length: Int
+    /** Create a copy of this array without the first element */
+    def tail: Arr[T]
+    /** Create a copy of this array by mapping all the elements with the given function */
+    def map[U](f: T => U): Arr[U]
+  }
+
+  object Arr {
+    /** Create an array with the given elements */
+    def apply[T](xs: T*): Arr[T] = {
+      val arr: Arr[T] = Arr.ofLength(xs.length)
+      for i <- 0 until xs.length do arr(i) = xs(i)
+      arr
+    }
+
+    /** Create an array with the given length. All elements are initialized to `null`. */
+    def ofLength[T](n: Int): Arr[T] =
+      newArrOfLength(n)
+
+  }
+
+  /** Create an array with the given length. All elements are initialized to `null`. */
+  def newArrOfLength[T](n: Int): Arr[T]
+
+  /** A number representing `radicand^(1.0/degree)` */
+  case class Root(radicand: Int, degree: Int) {
+    def toDouble: Double = scala.math.pow(radicand, 1.0/degree)
+  }
+
+  /** Tree result of an upsweep operation. Specialized for `Root` results. */
+  trait TreeRes { val res: Root }
+  /** Leaf result of an upsweep operation. Specialized for `Root` results. */
+  case class Leaf(from: Int, to: Int, res: Root) extends TreeRes
+  /** Tree node result of an upsweep operation. Specialized for `Root` results. */
+  case class Node(left: TreeRes, res: Root, right: TreeRes) extends TreeRes
+}
diff --git a/previous-exams/2021-midterm/m2/src/main/scala/m2/M2.scala b/previous-exams/2021-midterm/m2/src/main/scala/m2/M2.scala
new file mode 100644
index 0000000000000000000000000000000000000000..0fcaa856b7ce4c7615d1f3b3df58c330322a4c46
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/src/main/scala/m2/M2.scala
@@ -0,0 +1,89 @@
+package m2
+
+
+trait M2 extends Lib {
+  // Functions and classes of Lib can be used in here
+
+  /** Compute the rolling geometric mean of an array.
+   *
+   *  For an array `arr = Arr(x1, x2, x3, ..., xn)` the result is
+   *  `Arr(math.pow(x1, 1), math.pow((x1 + x2), 1.0/2), math.pow((x1 + x2 + x3), 1.0/3), ..., math.pow((x1 + x2 + x3 + ... + xn), 1.0/n))`
+   */
+  def rollingGeoMeanParallel(arr: Arr[Int]): Arr[Double] = {
+    if (arr.length == 0) return Arr.ofLength(0)
+    // TASK 1:  Add missing parallelization in `upsweep` and `downsweep`.
+    //          You should use the `parallel` method.
+    //          You should use the sequential version if the number of elements is lower than THRESHOLD.
+    // TASK 2a: Pass `arr` to `upsweep` and `downsweep` instead of `tmp`.
+    //          You will need to change some signatures and update the code appropriately.
+    //          Remove the definition of `tmp`
+    // TASK 2b: Change the type of the array `out` from `Root` to `Double`
+    //          You will need to change some signatures and update the code appropriately.
+    //          Remove the call `.map(root => root.toDouble)`.
+    // TASK 3:  Remove the call to `.tail`.
+    //          Update the update the code appropriately.
+
+    val tmp: Arr[Root] = arr.map(x => Root(x, 1))
+    val out: Arr[Root] = Arr.ofLength(arr.length + 1)
+    val tree = upsweep(tmp, 0, arr.length)
+    downsweep(tmp, Root(1, 0), tree, out)
+    out(0) = Root(1, 0)
+    out.map(root => root.toDouble).tail
+
+    // IDEAL SOLUTION
+    // val out = Arr.ofLength(arr.length)
+    // val tree = upsweep(arr, 0, arr.length)
+    // downsweep(arr, Root(1, 0), tree, out)
+    // out
+  }
+
+  def scanOp(acc: Root, x: Root) = // No need to modify this method
+    Root(acc.radicand * x.radicand, acc.degree + x.degree)
+
+  def upsweep(input: Arr[Root], from: Int, to: Int): TreeRes = {
+    if (to - from < 2)
+      Leaf(from, to, reduceSequential(input, from + 1, to, input(from)))
+    else {
+      val mid = from + (to - from) / 2
+      val (tL, tR) = (
+        upsweep(input, from, mid),
+        upsweep(input, mid, to)
+      )
+      Node(tL, scanOp(tL.res, tR.res), tR)
+    }
+  }
+
+  def downsweep(input: Arr[Root], a0: Root, tree: TreeRes, output: Arr[Root]): Unit = {
+    tree match {
+      case Node(left, _, right) =>
+        (
+          downsweep(input, a0, left, output),
+          downsweep(input, scanOp(a0, left.res), right, output)
+        )
+      case Leaf(from, to, _) =>
+        downsweepSequential(input, from, to, a0, output)
+    }
+  }
+
+  def downsweepSequential(input: Arr[Root], from: Int, to: Int, a0: Root, output: Arr[Root]): Unit = {
+    if (from < to) {
+      var i = from
+      var a = a0
+      while (i < to) {
+        a = scanOp(a, input(i))
+        i = i + 1
+        output(i) = a
+      }
+    }
+  }
+
+  def reduceSequential(input: Arr[Root], from: Int, to: Int, a0: Root): Root = {
+    var a = a0
+    var i = from
+    while (i < to) {
+      a = scanOp(a, input(i))
+      i = i + 1
+    }
+    a
+  }
+}
diff --git a/previous-exams/2021-midterm/m2/src/test/scala/m2/M2Suite.scala b/previous-exams/2021-midterm/m2/src/test/scala/m2/M2Suite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..a5b3a4461ea7db8a51a48de5ee0392971a61a3b6
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/src/test/scala/m2/M2Suite.scala
@@ -0,0 +1,175 @@
+package m2
+
+class M2Suite extends munit.FunSuite {
+
+  test("Rolling geometric mean result test (5pts)") {
+    RollingGeoMeanBasicLogicTest.basicTests()
+    RollingGeoMeanBasicLogicTest.normalTests()
+    RollingGeoMeanBasicLogicTest.largeTests()
+  }
+
+  test("[TASK 1] Rolling geometric mean parallelism test (30pts)") {
+    RollingGeoMeanCallsToParallel.parallelismTest()
+    RollingGeoMeanParallel.basicTests()
+    RollingGeoMeanParallel.normalTests()
+    RollingGeoMeanParallel.largeTests()
+  }
+
+  test("[TASK 2] Rolling geometric mean no `map` test (35pts)") {
+    RollingGeoMeanNoMap.basicTests()
+    RollingGeoMeanNoMap.normalTests()
+    RollingGeoMeanNoMap.largeTests()
+  }
+
+  test("[TASK 3] Rolling geometric mean no `tail` test (30pts)") {
+    RollingGeoMeanNoTail.basicTests()
+    RollingGeoMeanNoTail.normalTests()
+    RollingGeoMeanNoTail.largeTests()
+  }
+
+
+  object RollingGeoMeanBasicLogicTest extends M2 with LibImpl with RollingGeoMeanTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+  }
+
+  object RollingGeoMeanCallsToParallel extends M2 with LibImpl with RollingGeoMeanTest {
+    private var count = 0
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) =
+      count += 1
+      (op1, op2)
+
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+
+    def parallelismTest() = {
+      assertParallelCount(Arr(), 0)
+      assertParallelCount(Arr(1), 0)
+      assertParallelCount(Arr(1, 2, 3, 4), 0)
+      assertParallelCount(Arr(Array.tabulate(16)(identity): _*), 0)
+      assertParallelCount(Arr(Array.tabulate(32)(identity): _*), 0)
+
+      assertParallelCount(Arr(Array.tabulate(33)(identity): _*), 2)
+      assertParallelCount(Arr(Array.tabulate(64)(identity): _*), 2)
+      assertParallelCount(Arr(Array.tabulate(128)(identity): _*), 6)
+      assertParallelCount(Arr(Array.tabulate(256)(identity): _*), 14)
+      assertParallelCount(Arr(Array.tabulate(1000)(identity): _*), 62)
+      assertParallelCount(Arr(Array.tabulate(1024)(identity): _*), 62)
+    }
+
+    def assertParallelCount(arr: Arr[Int], expected: Int): Unit = {
+      try {
+        count = 0
+        rollingGeoMeanParallel(arr)
+        assert(count == expected, {
+          val extra = if (expected == 0) "" else s" ${expected/2} for the `upsweep` and ${expected/2} for the `downsweep`"
+          s"\n$arr\n\nERROR: Expected $expected instead of $count calls to `parallel(...)` for an array of ${arr.length} elements. Current parallel threshold is $THRESHOLD.$extra"
+        })
+      } finally {
+        count = 0
+      }
+    }
+
+  }
+
+  object RollingGeoMeanNoMap extends M2 with LibImpl with RollingGeoMeanTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) {
+      override def map[U](f: T => U): Arr[U] = throw Exception("Should not call Arr.map")
+    }
+  }
+
+  object RollingGeoMeanNoTail extends M2 with LibImpl with RollingGeoMeanTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) {
+      override def tail: Arr[T] = throw Exception("Should not call Arr.tail")
+    }
+  }
+
+  object RollingGeoMeanParallel extends M2 with LibImpl with RollingGeoMeanTest {
+    import scala.concurrent.duration._
+    val TIMEOUT = Duration(10, SECONDS)
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = {
+      import concurrent.ExecutionContext.Implicits.global
+      import scala.concurrent._
+      Await.result(Future(op1).zip(Future(op2)), TIMEOUT) // FIXME not timing-out
+    }
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+  }
+
+  trait LibImpl extends Lib {
+
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T]
+
+    def newArrOfLength[T](n: Int): Arr[T] =
+      newArrFrom(new Array(n))
+
+    class ArrImpl[T](val arr: Array[AnyRef]) extends Arr[T]:
+      def apply(i: Int): T =
+        arr(i).asInstanceOf[T]
+      def update(i: Int, x: T): Unit =
+        arr(i) = x.asInstanceOf[AnyRef]
+      def length: Int =
+        arr.length
+      def map[U](f: T => U): Arr[U] =
+        newArrFrom(arr.map(f.asInstanceOf[AnyRef => AnyRef]))
+      def tail: Arr[T] =
+        newArrFrom(arr.tail)
+      override def toString: String =
+        arr.mkString("Arr(", ", ", ")")
+      override def equals(that: Any): Boolean =
+        that match
+          case that: ArrImpl[_] => Array.equals(arr, that.arr)
+          case _ => false
+  }
+
+  trait RollingGeoMeanTest extends M2 {
+
+    def tabulate[T](n: Int)(f: Int => T): Arr[T] =
+      val arr = Arr.ofLength[T](n)
+      for i <- 0 until n do
+        arr(i) = f(i)
+      arr
+
+    def asSeq(arr: Arr[Double]) =
+      val array = new Array[Double](arr.length)
+      for i <- 0 to (arr.length - 1) do
+        array(i) = arr(i)
+      array.toSeq
+
+    def scanOp_(acc: Root, x: Root) =
+      Root(acc.radicand * x.radicand, acc.degree + x.degree)
+
+    def result(ds: Seq[Int]): Arr[Double] =
+      Arr(ds.map(x => Root(x, 1)).scan(Root(1, 0))(scanOp_).tail.map(_.toDouble): _*)
+
+    def check(input: Seq[Int]) =
+      assertEquals(
+        // .toString calls are a terrible kludge so that NaNs compare equal to eachother...
+        asSeq(rollingGeoMeanParallel(Arr(input: _*))).map(_.toString),
+        asSeq(result(input)).map(_.toString)
+      )
+
+    def basicTests() = {
+      check(Seq())
+      check(Seq(1))
+      check(Seq(1, 2, 3, 4))
+      check(Seq(4, 4, 4, 4))
+    }
+
+    def normalTests() = {
+      check(Seq.tabulate(64)(identity))
+      check(Seq(4, 4, 4, 4))
+      check(Seq(4, 8, 6, 4))
+      check(Seq(4, 3, 2, 1))
+      check(Seq.tabulate(64)(identity).reverse)
+      check(Seq.tabulate(128)(i => 128 - 2*i).reverse)
+    }
+
+    def largeTests() = {
+      check(Seq.tabulate(500)(identity))
+      check(Seq.tabulate(512)(identity))
+      check(Seq.tabulate(1_000)(identity))
+      check(Seq.tabulate(10_000)(identity))
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm-solutions/m20.md b/previous-exams/2021-midterm/m20.md
similarity index 95%
rename from previous-exams/2021-midterm-solutions/m20.md
rename to previous-exams/2021-midterm/m20.md
index 59f6c09d903ebea6a9c020a96a176032387c186a..78f1d3f2b5e0fa4cb7dd4da8f002402974dd2c39 100644
--- a/previous-exams/2021-midterm-solutions/m20.md
+++ b/previous-exams/2021-midterm/m20.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m20 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m20
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m20/.gitignore b/previous-exams/2021-midterm/m20/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m20/assignment.sbt b/previous-exams/2021-midterm/m20/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m20/build.sbt b/previous-exams/2021-midterm/m20/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8cd8c7a0320af71ee8405888adcaa8556c98574f
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m20"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m20.M20Suite"
diff --git a/previous-exams/2021-midterm/m20/grading-tests.jar b/previous-exams/2021-midterm/m20/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..89112fdcd241e77ac54effcbaeb3c631caf37fa0
Binary files /dev/null and b/previous-exams/2021-midterm/m20/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m20/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m20/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m20/project/MOOCSettings.scala b/previous-exams/2021-midterm/m20/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m20/project/StudentTasks.scala b/previous-exams/2021-midterm/m20/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m20/project/build.properties b/previous-exams/2021-midterm/m20/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m20/project/buildSettings.sbt b/previous-exams/2021-midterm/m20/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m20/project/plugins.sbt b/previous-exams/2021-midterm/m20/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/MultiWriterSeqCount.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/MultiWriterSeqCount.scala
new file mode 100644
index 0000000000000000000000000000000000000000..7bb3a657410f5e706a9bf890b3e8d0483b445f45
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/MultiWriterSeqCount.scala
@@ -0,0 +1,46 @@
+package m20
+
+import instrumentation._
+
+import scala.annotation.tailrec
+
+/** Multi-writer, multi-reader data structure containing a pair of integers. */
+class MultiWriterSeqCount extends Monitor:
+  /** Do not directly use this variable, use `generation`, `setGeneration` and
+   *  `compareAndSetGeneration` instead.
+   */
+  protected val myGeneration: AbstractAtomicVariable[Int] = new AtomicVariable(0)
+  protected def generation: Int = myGeneration.get
+  protected def setGeneration(newGeneration: Int): Unit =
+    myGeneration.set(newGeneration)
+  protected def compareAndSetGeneration(expected: Int, newValue: Int): Boolean =
+    myGeneration.compareAndSet(expected, newValue)
+
+  /** Do not directly use this variable, use `x` and `setX` instead. */
+  protected var myX: Int = 0
+  protected def x: Int = myX
+  protected def setX(newX: Int): Unit =
+    myX = newX
+
+  /** Do not directly use this variable, use `y` and `setY` instead. */
+  protected var myY: Int = 0
+  protected def y: Int = myY
+  protected def setY(newY: Int): Unit =
+    myY = newY
+
+  /** Write new values into this data structure.
+   *  This method is always safe to call.
+   *  The implementation of this method is not allowed to call `synchronized`.
+   */
+  final def write(newX: Int, newY: Int): Unit = ???
+
+  /** Copy the values previously written into this data structure into a tuple.
+   *  This method is always safe to call.
+   *  The implementation of this method is not allowed to call `synchronized`.
+   */
+  final def copy(): (Int, Int) =
+    // You should be able to just copy-paste the implementation of `copy` you
+    // wrote in `SeqCount` here.
+    ???
+
+end MultiWriterSeqCount
diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/SeqCount.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/SeqCount.scala
new file mode 100644
index 0000000000000000000000000000000000000000..3003be40dd526ae89aa5ec93a7c2a6b7286d2d7e
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/SeqCount.scala
@@ -0,0 +1,42 @@
+package m20
+
+import instrumentation._
+
+import scala.annotation.tailrec
+
+/** Single-writer, multi-reader data structure containing a pair of integers. */
+class SeqCount extends Monitor:
+  /** Do not directly use this variable, use `generation` and `setGeneration` instead. */
+  @volatile protected var myGeneration: Int = 0
+  protected def generation: Int = myGeneration
+  protected def setGeneration(newGeneration: Int): Unit =
+    myGeneration = newGeneration
+
+  /** Do not directly use this variable, use `x` and `setX` instead. */
+  protected var myX: Int = 0
+  protected def x: Int = myX
+  protected def setX(newX: Int): Unit =
+    myX = newX
+
+  /** Do not directly use this variable, use `y` and `setY` instead. */
+  protected var myY: Int = 0
+  protected def y: Int = myY
+  protected def setY(newY: Int): Unit =
+    myY = newY
+
+  /** Write new values into this data structure.
+   *  This method must only be called from one thread at a time.
+   */
+  final def write(newX: Int, newY: Int): Unit =
+    setGeneration(generation + 1)
+    setX(newX)
+    setY(newY)
+    setGeneration(generation + 1)
+
+  /** Copy the values previously written into this data structure into a tuple.
+   *  This method is always safe to call.
+   *  The implementation of this method is not allowed to call `synchronized`.
+   */
+  final def copy(): (Int, Int) = ???
+
+end SeqCount
diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/AtomicVariable.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/AtomicVariable.scala
new file mode 100644
index 0000000000000000000000000000000000000000..b96628365180b1e0c75d77f7adcdd9e56f718d3c
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/AtomicVariable.scala
@@ -0,0 +1,28 @@
+package m20.instrumentation
+
+import java.util.concurrent.atomic._
+
+abstract class AbstractAtomicVariable[T] {
+  def get: T
+  def set(value: T): Unit
+  def compareAndSet(expect: T, newval: T) : Boolean
+}
+
+class AtomicVariable[T](initial: T) extends AbstractAtomicVariable[T] {
+
+  private val atomic = new AtomicReference[T](initial)
+
+  override def get: T = atomic.get()
+
+  override def set(value: T): Unit = atomic.set(value)
+
+  override def compareAndSet(expected: T, newValue: T): Boolean = {
+    val current = atomic.get
+    if (current == expected) {
+      atomic.compareAndSet(current, newValue)
+    }
+    else {
+      false
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/Monitor.scala b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/Monitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..bf844ace3980d4a1da1d59c4f80870c38c563dc6
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/instrumentation/Monitor.scala
@@ -0,0 +1,23 @@
+package m20.instrumentation
+
+class Dummy
+
+trait Monitor {
+  implicit val dummy: Dummy = new Dummy
+
+  def wait()(implicit i: Dummy) = waitDefault()
+
+  def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e)
+
+  def notify()(implicit i: Dummy) = notifyDefault()
+
+  def notifyAll()(implicit i: Dummy) = notifyAllDefault()
+
+  private val lock = new AnyRef
+
+  // Can be overriden.
+  def waitDefault(): Unit = lock.wait()
+  def synchronizedDefault[T](toExecute: =>T): T = lock.synchronized(toExecute)
+  def notifyDefault(): Unit = lock.notify()
+  def notifyAllDefault(): Unit = lock.notifyAll()
+}
diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/TestSuite.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/TestSuite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..14e707c78c31a466e107cabf565f8c1f2b8ddb75
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/TestSuite.scala
@@ -0,0 +1,122 @@
+package m20
+
+import scala.concurrent._
+import scala.concurrent.duration._
+import scala.collection.mutable.HashMap
+import scala.util.Random
+import instrumentation._
+import instrumentation.TestHelper._
+import instrumentation.TestUtils._
+
+enum ThreadResult:
+  case WriteError(error: String)
+  case WriteSuccess
+  case Read(result: (Int, Int))
+import ThreadResult._
+
+class M20Suite extends munit.FunSuite:
+  /** If at least one thread resulted in an error,
+   *  return `(false, errorMessage)` otherwise return `(true, "")`.
+   */
+  def processResults(results: List[ThreadResult]): (Boolean, String) =
+    val success = (true, "")
+    results.foldLeft(success) {
+      case (acc @ (false, _), _) =>
+        // Report the first error found
+        acc
+      case (_, WriteError(error)) =>
+        (false, error)
+      case (_, Read((x, y))) if x + 1 != y =>
+        (false, s"Read ($x, $y) but expected y to be ${x + 1}")
+      case (_, _: Read | WriteSuccess) =>
+        success
+    }
+
+  def randomList(length: Int): List[Int] =
+    List.fill(length)(Random.nextInt)
+
+  test("SeqCount: single-threaded write and copy (1 pts)") {
+    val sc = new SeqCount
+    randomList(100).lazyZip(randomList(100)).foreach { (x, y) =>
+      sc.write(x, y)
+      assertEquals(sc.copy(), (x, y))
+    }
+  }
+
+  test("SeqCount: one write thread, two copy threads (4 pts)") {
+    testManySchedules(3, sched =>
+      val sc = new SchedulableSeqCount(sched)
+      // Invariant in this test: y == x + 1
+      sc.write(0, 1)
+
+      val randomValues = randomList(length = 5)
+
+      def writeThread(): ThreadResult =
+        randomValues.foldLeft(WriteSuccess) {
+          case (res: WriteError, _) =>
+            // Report the first error found
+            res
+          case (_, i) =>
+            sc.write(i, i + 1)
+            val writtenValues = (i, i + 1)
+            val readBack = sc.copy()
+            if writtenValues != readBack then
+              WriteError(s"Wrote $writtenValues but read back $readBack")
+            else
+              WriteSuccess
+        }
+
+      def copyThread(): ThreadResult =
+        Read(sc.copy())
+
+      val threads = List(
+        () => writeThread(),
+        () => copyThread(),
+        () => copyThread()
+      )
+
+      (threads, results => processResults(results.asInstanceOf[List[ThreadResult]]))
+    )
+  }
+
+  test("MultiWriterSeqCount: single-threaded write and copy (1 pts)") {
+    val sc = new MultiWriterSeqCount
+    randomList(100).lazyZip(randomList(100)).foreach { (x, y) =>
+      sc.write(x, y)
+      assertEquals(sc.copy(), (x, y))
+    }
+  }
+
+  test("MultiWriterSeqCount: two write threads, two copy threads (4 pts)") {
+    testManySchedules(4, sched =>
+      val msc = new SchedulableMultiWriterSeqCount(sched)
+      // Invariant in this test: y == x + 1
+      msc.write(0, 1)
+
+      val randomValues = randomList(length = 5)
+
+      def writeThread(): ThreadResult =
+        randomValues.foreach(i => msc.write(i, i + 1))
+        // Unlke in the SeqCount test, we do not verify that we can read back
+        // the values we wrote, because the other writer thread might have
+        // overwritten them already.
+        WriteSuccess
+
+      def copyThread(): ThreadResult =
+        Read(msc.copy())
+
+      val threads = List(
+        () => writeThread(),
+        () => writeThread(),
+        () => copyThread(),
+        () => copyThread()
+      )
+
+      (threads, results => processResults(results.asInstanceOf[List[ThreadResult]]))
+    )
+  }
+
+  import scala.concurrent.duration._
+  override val munitTimeout = 200.seconds
+end M20Suite
+
diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/MockedMonitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..0b7e94fd37926fbc2c84b4a5ca8c7113a9b9d8fc
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/MockedMonitor.scala
@@ -0,0 +1,72 @@
+package m20.instrumentation
+
+trait MockedMonitor extends Monitor {
+  def scheduler: Scheduler
+  
+  // Can be overriden.
+  override def waitDefault() = {
+    scheduler.log("wait")
+    scheduler updateThreadState Wait(this, scheduler.threadLocks.tail)
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    scheduler.log("synchronized check") 
+    val prevLocks = scheduler.threadLocks
+    scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue.
+    scheduler.log("synchronized -> enter")
+    try {
+      toExecute
+    } finally {
+      scheduler updateThreadState Running(prevLocks)
+      scheduler.log("synchronized -> out")
+    }    
+  }
+  override def notifyDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notify")
+  }
+  override def notifyAllDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notifyAll")
+  }
+}
+
+trait LockFreeMonitor extends Monitor {
+  override def waitDefault() = {
+    throw new Exception("Please use lock-free structures and do not use wait()")
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    throw new Exception("Please use lock-free structures and do not use synchronized()")
+  }
+  override def notifyDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notify()")
+  }
+  override def notifyAllDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notifyAll()")
+  }
+}
+
+
+abstract class ThreadState {
+  def locks: Seq[AnyRef]
+}
+trait CanContinueIfAcquiresLock extends ThreadState {
+  def lockToAquire: AnyRef
+}
+case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState
+case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Running(locks: Seq[AnyRef]) extends ThreadState
+case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState
diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Scheduler.scala
new file mode 100644
index 0000000000000000000000000000000000000000..bdc09b50f0d8ab05e89a06654034c855f98e9b3d
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Scheduler.scala
@@ -0,0 +1,304 @@
+package m20.instrumentation
+
+import java.util.concurrent._;
+import scala.concurrent.duration._
+import scala.collection.mutable._
+import Stats._
+
+import java.util.concurrent.atomic.AtomicInteger
+
+sealed abstract class Result
+case class RetVal(rets: List[Any]) extends Result
+case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result
+case class Timeout(msg: String) extends Result
+
+/**
+ * A class that maintains schedule and a set of thread ids.
+ * The schedules are advanced after an operation of a SchedulableBuffer is performed.
+ * Note: the real schedule that is executed may deviate from the input schedule
+ * due to the adjustments that had to be made for locks
+ */
+class Scheduler(sched: List[Int]) {
+  val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform
+
+  private var schedule = sched
+  private var numThreads = 0
+  private val realToFakeThreadId = Map[Long, Int]()
+  private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat)
+  private val threadStates = Map[Int, ThreadState]()
+
+  /**
+   * Runs a set of operations in parallel as per the schedule.
+   * Each operation may consist of many primitive operations like reads or writes
+   * to shared data structure each of which should be executed using the function `exec`.
+   * @timeout in milliseconds
+   * @return true - all threads completed on time,  false -some tests timed out.
+   */
+  def runInParallel(timeout: Long, ops: List[() => Any]): Result = {
+    numThreads = ops.length
+    val threadRes = Array.fill(numThreads) { None: Any }
+    var exception: Option[Except] = None
+    val syncObject = new Object()
+    var completed = new AtomicInteger(0)
+    // create threads
+    val threads = ops.zipWithIndex.map {
+      case (op, i) =>
+        new Thread(new Runnable() {
+          def run(): Unit = {
+            val fakeId = i + 1
+            setThreadId(fakeId)
+            try {
+              updateThreadState(Start)
+              val res = op()
+              updateThreadState(End)
+              threadRes(i) = res
+              // notify the master thread if all threads have completed
+              if (completed.incrementAndGet() == ops.length) {
+                syncObject.synchronized { syncObject.notifyAll() }
+              }
+            } catch {
+              case e: Throwable if exception != None => // do nothing here and silently fail
+              case e: Throwable =>
+                log(s"throw ${e.toString}")
+                exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"),
+                    e.getStackTrace))
+                syncObject.synchronized { syncObject.notifyAll() }
+              //println(s"$fakeId: ${e.toString}")
+              //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads)
+            }
+          }
+        })
+    }
+    // start all threads
+    threads.foreach(_.start())
+    // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire
+    var remTime = timeout
+    syncObject.synchronized {
+      timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time }
+    }
+    if (exception.isDefined) {
+      exception.get
+    } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors
+      Timeout(opLog.mkString("\n"))
+    } else {
+      // every thing executed normally
+      RetVal(threadRes.toList)
+    }
+  }
+
+  // Updates the state of the current thread
+  def updateThreadState(state: ThreadState): Unit = {
+    val tid = threadId
+    synchronized {
+      threadStates(tid) = state
+    }
+    state match {
+      case Sync(lockToAquire, locks) =>
+        if (locks.indexOf(lockToAquire) < 0) waitForTurn else {
+          // Re-aqcuiring the same lock
+          updateThreadState(Running(lockToAquire +: locks))
+        }
+      case Start      => waitStart()
+      case End        => removeFromSchedule(tid)
+      case Running(_) =>
+      case _          => waitForTurn // Wait, SyncUnique, VariableReadWrite
+    }
+  }
+
+  def waitStart(): Unit = {
+    //while (threadStates.size < numThreads) {
+    //Thread.sleep(1)
+    //}
+    synchronized {
+      if (threadStates.size < numThreads) {
+        wait()
+      } else {
+        notifyAll()
+      }
+    }
+  }
+
+  def threadLocks = {
+    synchronized {
+      threadStates(threadId).locks
+    }
+  }
+
+  def threadState = {
+    synchronized {
+      threadStates(threadId)
+    }
+  }
+
+  def mapOtherStates(f: ThreadState => ThreadState) = {
+    val exception = threadId
+    synchronized {
+      for (k <- threadStates.keys if k != exception) {
+        threadStates(k) = f(threadStates(k))
+      }
+    }
+  }
+
+  def log(str: String) = {
+    if((realToFakeThreadId contains Thread.currentThread().getId())) {
+      val space = (" " * ((threadId - 1) * 2))
+      val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + "  ")
+      opLog += s
+    }
+  }
+
+  /**
+   * Executes a read or write operation to a global data structure as per the given schedule
+   * @param msg a message corresponding to the operation that will be logged
+   */
+  def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = {
+    if(! (realToFakeThreadId contains Thread.currentThread().getId())) {
+      primop
+    } else {
+      updateThreadState(VariableReadWrite(threadLocks))
+      val m = msg
+      if(m != "") log(m)
+      if (opLog.size > maxOps)
+        throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!")
+      val res = primop
+      postMsg match {
+        case Some(m) => log(m(res))
+        case None =>
+      }
+      res
+    }
+  }
+
+  private def setThreadId(fakeId: Int) = synchronized {
+    realToFakeThreadId(Thread.currentThread.getId) = fakeId
+  }
+
+  def threadId =
+    try {
+      realToFakeThreadId(Thread.currentThread().getId())
+    } catch {
+    case e: NoSuchElementException =>
+      throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!")
+    }
+
+  private def isTurn(tid: Int) = synchronized {
+    (!schedule.isEmpty && schedule.head != tid)
+  }
+
+  def canProceed(): Boolean = {
+    val tid = threadId
+    canContinue match {
+      case Some((i, state)) if i == tid =>
+        //println(s"$tid: Runs ! Was in state $state")
+        canContinue = None
+        state match {
+          case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks))
+          case SyncUnique(lockToAquire, locks) =>
+            mapOtherStates {
+              _ match {
+                case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2)
+                case e => e
+              }
+            }
+            updateThreadState(Running(lockToAquire +: locks))
+          case VariableReadWrite(locks) => updateThreadState(Running(locks))
+        }
+        true
+      case Some((i, state)) =>
+        //println(s"$tid: not my turn but $i !")
+        false
+      case None =>
+        false
+    }
+  }
+
+  var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute.
+
+  /** returns true if the thread can continue to execute, and false otherwise */
+  def decide(): Option[(Int, ThreadState)] = {
+    if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision.
+      //println(s"$threadId: I'm taking a decision")
+      if (threadStates.values.forall { case e: Wait => true case _ => false }) {
+        val waiting = threadStates.keys.map(_.toString).mkString(", ")
+        val s = if (threadStates.size > 1) "s" else ""
+        val are = if (threadStates.size > 1) "are" else "is"
+        throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.")
+      } else {
+        // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode.
+        // Let's determine which ones can continue.
+        val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet
+        val threadsNotBlocked = threadStates.toSeq.filter {
+          case (id, v: VariableReadWrite)         => true
+          case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire)
+          case _                                  => false
+        }
+        if (threadsNotBlocked.isEmpty) {
+          val waiting = threadStates.keys.map(_.toString).mkString(", ")
+          val s = if (threadStates.size > 1) "s" else ""
+          val are = if (threadStates.size > 1) "are" else "is"
+          val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap
+          val reason = threadStates.collect {
+            case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) =>
+              s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}"
+          }.mkString("\n")
+          throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason")
+        } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute.
+          Some(threadsNotBlocked(0))
+        } else {
+          val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t })
+          if (next != -1) {
+            //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}")
+            val chosenOne = schedule(next) // TODO: Make schedule a mutable list.
+            schedule = schedule.take(next) ++ schedule.drop(next + 1)
+            Some((chosenOne, threadStates(chosenOne)))
+          } else {
+            threadPreference = (threadPreference + 1) % threadsNotBlocked.size
+            val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy
+            Some(chosenOne)
+            //threadsNotBlocked.indexOf(threadId) >= 0
+            /*
+            val tnb = threadsNotBlocked.map(_._1).mkString(",")
+            val s = if (schedule.isEmpty) "empty" else schedule.mkString(",")
+            val only = if (schedule.isEmpty) "" else " only"
+            throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/
+          }
+        }
+      }
+    } else canContinue
+  }
+
+  /**
+   * This will be called before a schedulable operation begins.
+   * This should not use synchronized
+   */
+  var numThreadsWaiting = new AtomicInteger(0)
+  //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice.
+  var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue.
+  private def waitForTurn = {
+    synchronized {
+      if (numThreadsWaiting.incrementAndGet() == threadStates.size) {
+        canContinue = decide()
+        notifyAll()
+      }
+      //waitingForDecision(threadId) = Some(numThreadsWaiting)
+      //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}")
+      while (!canProceed()) wait()
+    }
+    numThreadsWaiting.decrementAndGet()
+  }
+
+  /**
+   * To be invoked when a thread is about to complete
+   */
+  private def removeFromSchedule(fakeid: Int) = synchronized {
+    //println(s"$fakeid: I'm taking a decision because I finished")
+    schedule = schedule.filterNot(_ == fakeid)
+    threadStates -= fakeid
+    if (numThreadsWaiting.get() == threadStates.size) {
+      canContinue = decide()
+      notifyAll()
+    }
+  }
+
+  def getOperationLog() = opLog
+}
diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Stats.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Stats.scala
new file mode 100644
index 0000000000000000000000000000000000000000..e38dd2ab10b997f3e8d89fd8b9806bbb8c597e75
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/Stats.scala
@@ -0,0 +1,23 @@
+/* Copyright 2009-2015 EPFL, Lausanne */
+package m20.instrumentation
+
+import java.lang.management._
+
+/**
+ * A collection of methods that can be used to collect run-time statistics about Leon programs.
+ * This is mostly used to test the resources properties of Leon programs
+ */
+object Stats {
+  def timed[T](code: => T)(cont: Long => Unit): T = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    cont((System.currentTimeMillis() - t1))
+    r
+  }
+
+  def withTime[T](code: => T): (T, Long) = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    (r, (System.currentTimeMillis() - t1))
+  }
+}
diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestHelper.scala
new file mode 100644
index 0000000000000000000000000000000000000000..b361ded58992134d41995527014ca62133b7e4d3
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestHelper.scala
@@ -0,0 +1,124 @@
+package m20.instrumentation
+
+import scala.util.Random
+import scala.collection.mutable.{Map => MutableMap}
+
+import Stats._
+
+object TestHelper {
+  val noOfSchedules = 10000 // set this to 100k during deployment
+  val readWritesPerThread = 20 // maximum number of read/writes possible in one thread
+  val contextSwitchBound = 10
+  val testTimeout = 150 // the total time out for a test in seconds
+  val schedTimeout = 15 // the total time out for execution of a schedule in secs
+
+  // Helpers
+  /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1))
+  def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/
+
+  def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) =
+    testManySchedules(1,
+      (sched: Scheduler) => {
+        (List(() => ops(sched)),
+         (res: List[Any]) => assertions(res.head.asInstanceOf[T]))
+      })
+
+  /**
+   * @numThreads number of threads
+   * @ops operations to be executed, one per thread
+   * @assertion as condition that will executed after all threads have completed (without exceptions)
+   * 					 the arguments are the results of the threads
+   */
+  def testManySchedules(numThreads: Int,
+      ops: Scheduler =>
+        (List[() => Any], // Threads
+         List[Any] => (Boolean, String)) // Assertion
+      ) = {
+    var timeout = testTimeout * 1000L
+    val threadIds = (1 to numThreads)
+    //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach {
+    val schedules = (new ScheduleGenerator(numThreads)).schedules()
+    var schedsExplored = 0
+    schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach {
+      //case _ if timeout <= 0 => // break
+      case schedule =>
+        schedsExplored += 1
+        val schedr = new Scheduler(schedule)
+        //println("Exploring Sched: "+schedule)
+        val (threadOps, assertion) = ops(schedr)
+        if (threadOps.size != numThreads)
+          throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps")
+        timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match {
+          case Timeout(msg) =>
+            throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg)
+          case Except(msg, stkTrace) =>
+            val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n")
+            throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr)
+          case RetVal(threadRes) =>
+            // check the assertion
+            val (success, custom_msg) = assertion(threadRes)
+            if (!success) {
+              val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n")
+              throw new java.lang.AssertionError("Assertion failed: "+msg)
+            }
+        }
+    }
+    if (timeout <= 0) {
+      throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!")
+    }
+  }
+
+  /**
+   * A schedule generator that is based on the context bound
+   */
+  class ScheduleGenerator(numThreads: Int) {
+    val scheduleLength = readWritesPerThread * numThreads
+    val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position
+    def schedules(): LazyList[List[Int]] = {
+      var contextSwitches = 0
+      var contexts = List[Int]() // a stack of thread ids in the order of context-switches
+      val remainingOps = MutableMap[Int, Int]()
+      remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread
+      val liveThreads = (1 to numThreads).toSeq.toBuffer
+
+      /**
+       * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule
+       */
+      def updateState(tid: Int): Unit = {
+        val remOps = remainingOps(tid)
+        if (remOps == 0) {
+          liveThreads -= tid
+        } else {
+          remainingOps += (tid -> (remOps - 1))
+        }
+      }
+      val schedule = rands.foldLeft(List[Int]()) {
+        case (acc, r) if contextSwitches < contextSwitchBound =>
+          val tid = liveThreads(r.nextInt(liveThreads.size))
+          contexts match {
+            case prev :: tail if prev != tid => // we have a new context switch here
+              contexts +:= tid
+              contextSwitches += 1
+            case prev :: tail =>
+            case _ => // init case
+              contexts +:= tid
+          }
+          updateState(tid)
+          acc :+ tid
+        case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches
+          if (!contexts.isEmpty) {
+            contexts = contexts.dropWhile(remainingOps(_) == 0)
+          }
+          val tid = contexts match {
+            case top :: tail => top
+            case _ => liveThreads(0)  // here, there has to be threads that have not even started
+          }
+          updateState(tid)
+          acc :+ tid
+      }
+      schedule #:: schedules()
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestUtils.scala
new file mode 100644
index 0000000000000000000000000000000000000000..a6a1cac480c1547251b8782042706083cbd3ce4e
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/instrumentation/TestUtils.scala
@@ -0,0 +1,19 @@
+package m20.instrumentation
+
+import scala.concurrent._
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object TestUtils {
+  def failsOrTimesOut[T](action: => T): Boolean = {
+    val asyncAction = Future {
+      action
+    }
+    try {
+      Await.result(asyncAction, 2000.millisecond)
+    } catch {
+      case _: Throwable => return true
+    }
+    return false
+  }
+}
diff --git a/previous-exams/2021-midterm/m20/src/test/scala/m20/overrides.scala b/previous-exams/2021-midterm/m20/src/test/scala/m20/overrides.scala
new file mode 100644
index 0000000000000000000000000000000000000000..cffe5c598c82933dd3a7f7bc00d810d32c582bc6
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/test/scala/m20/overrides.scala
@@ -0,0 +1,68 @@
+package m20
+
+import instrumentation._
+
+import scala.annotation.tailrec
+import java.util.concurrent.atomic._
+
+class SchedulableAtomicVariable[T](initial: T, scheduler: Scheduler, name: String) extends AbstractAtomicVariable[T]:
+  private val proxied: AtomicVariable[T] = new AtomicVariable[T](initial)
+
+  override def get: T = scheduler.exec {
+    proxied.get
+  } (s"", Some(res => s"$name: get $res"))
+
+  override def set(value: T): Unit = scheduler.exec {
+    proxied.set(value)
+  } (s"$name: set $value", None)
+
+  override def compareAndSet(expected: T, newValue: T): Boolean = {
+    scheduler.exec {
+      proxied.compareAndSet(expected, newValue)
+    } (s"$name: compareAndSet(expected = $expected, newValue = $newValue)", Some(res => s"$name: Did it set? $res") )
+  }
+
+end SchedulableAtomicVariable
+
+class SchedulableSeqCount(val scheduler: Scheduler) extends SeqCount with LockFreeMonitor:
+  override def generation: Int = scheduler.exec {
+    super.generation
+  } ("", Some(res => s"generation is $res"))
+  override def setGeneration(newGeneration: Int): Unit = scheduler.exec {
+    super.setGeneration(newGeneration)
+  } ( s"setGeneration($newGeneration)", None )
+
+  override def x: Int = scheduler.exec {
+    super.x
+  } ("", Some(res => s"x is $res"))
+  override def setX(newX: Int): Unit = scheduler.exec {
+    super.setX(newX)
+  } (s"setX($newX)", None)
+
+  override def y: Int = scheduler.exec {
+    super.y
+  } ("", Some(res => s"y is $res"))
+  override def setY(newY: Int): Unit = scheduler.exec {
+    super.setY(newY)
+  } (s"setY($newY)", None)
+
+end SchedulableSeqCount
+
+class SchedulableMultiWriterSeqCount(val scheduler: Scheduler) extends MultiWriterSeqCount with LockFreeMonitor:
+  override protected val myGeneration: AbstractAtomicVariable[Int] = new SchedulableAtomicVariable(0, scheduler, "myGeneration")
+
+  override def x: Int = scheduler.exec {
+    super.x
+  } ("", Some(res => s"x is $res"))
+  override def setX(newX: Int): Unit = scheduler.exec {
+    super.setX(newX)
+  } (s"setX($newX)", None)
+
+  override def y: Int = scheduler.exec {
+    super.y
+  } ("", Some(res => s"y is $res"))
+  override def setY(newY: Int): Unit = scheduler.exec {
+    super.setY(newY)
+  } (s"setY($newY)", None)
+
+end SchedulableMultiWriterSeqCount
diff --git a/previous-exams/2021-midterm-solutions/m21.md b/previous-exams/2021-midterm/m21.md
similarity index 95%
rename from previous-exams/2021-midterm-solutions/m21.md
rename to previous-exams/2021-midterm/m21.md
index fb11db8278973e5a3f9f6a660a477e5f792f5bae..82736a23664c7abba6312b6df914fedc297faf04 100644
--- a/previous-exams/2021-midterm-solutions/m21.md
+++ b/previous-exams/2021-midterm/m21.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m21 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m21
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m21/.gitignore b/previous-exams/2021-midterm/m21/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m21/assignment.sbt b/previous-exams/2021-midterm/m21/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m21/build.sbt b/previous-exams/2021-midterm/m21/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..15c62589add717bed6163f0f77d5d607033187bf
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m21"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m21.M21Suite"
diff --git a/previous-exams/2021-midterm/m21/grading-tests.jar b/previous-exams/2021-midterm/m21/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..a8c37ec97d20d360071bd45af4e344e62720698f
Binary files /dev/null and b/previous-exams/2021-midterm/m21/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m21/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m21/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m21/project/MOOCSettings.scala b/previous-exams/2021-midterm/m21/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m21/project/StudentTasks.scala b/previous-exams/2021-midterm/m21/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m21/project/build.properties b/previous-exams/2021-midterm/m21/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m21/project/buildSettings.sbt b/previous-exams/2021-midterm/m21/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m21/project/plugins.sbt b/previous-exams/2021-midterm/m21/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/MultiWriterSeqCount.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/MultiWriterSeqCount.scala
new file mode 100644
index 0000000000000000000000000000000000000000..bec39210798debbcfada52cddc1d291d2c41d891
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/MultiWriterSeqCount.scala
@@ -0,0 +1,46 @@
+package m21
+
+import instrumentation._
+
+import scala.annotation.tailrec
+
+/** Multi-writer, multi-reader data structure containing a pair of integers. */
+class MultiWriterSeqCount extends Monitor:
+  /** Do not directly use this variable, use `generation`, `setGeneration` and
+   *  `compareAndSetGeneration` instead.
+   */
+  protected val myGeneration: AbstractAtomicVariable[Int] = new AtomicVariable(1)
+  protected def generation: Int = myGeneration.get
+  protected def setGeneration(newGeneration: Int): Unit =
+    myGeneration.set(newGeneration)
+  protected def compareAndSetGeneration(expected: Int, newValue: Int): Boolean =
+    myGeneration.compareAndSet(expected, newValue)
+
+  /** Do not directly use this variable, use `x` and `setX` instead. */
+  protected var myX: Int = 0
+  protected def x: Int = myX
+  protected def setX(newX: Int): Unit =
+    myX = newX
+
+  /** Do not directly use this variable, use `y` and `setY` instead. */
+  protected var myY: Int = 0
+  protected def y: Int = myY
+  protected def setY(newY: Int): Unit =
+    myY = newY
+
+  /** Write new values into this data structure.
+   *  This method is always safe to call.
+   *  The implementation of this method is not allowed to call `synchronized`.
+   */
+  final def write(newX: Int, newY: Int): Unit = ???
+
+  /** Copy the values previously written into this data structure into a tuple.
+   *  This method is always safe to call.
+   *  The implementation of this method is not allowed to call `synchronized`.
+   */
+  final def copy(): (Int, Int) =
+    // You should be able to just copy-paste the implementation of `copy` you
+    // wrote in `SeqCount` here.
+    ???
+
+end MultiWriterSeqCount
diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/SeqCount.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/SeqCount.scala
new file mode 100644
index 0000000000000000000000000000000000000000..f7a50513af3aebe478f9b9bd54ae77f919244728
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/SeqCount.scala
@@ -0,0 +1,42 @@
+package m21
+
+import instrumentation._
+
+import scala.annotation.tailrec
+
+/** Single-writer, multi-reader data structure containing a pair of integers. */
+class SeqCount extends Monitor:
+  /** Do not directly use this variable, use `generation` and `setGeneration` instead. */
+  @volatile protected var myGeneration: Int = 1
+  protected def generation: Int = myGeneration
+  protected def setGeneration(newGeneration: Int): Unit =
+    myGeneration = newGeneration
+
+  /** Do not directly use this variable, use `x` and `setX` instead. */
+  protected var myX: Int = 0
+  protected def x: Int = myX
+  protected def setX(newX: Int): Unit =
+    myX = newX
+
+  /** Do not directly use this variable, use `y` and `setY` instead. */
+  protected var myY: Int = 0
+  protected def y: Int = myY
+  protected def setY(newY: Int): Unit =
+    myY = newY
+
+  /** Write new values into this data structure.
+   *  This method must only be called from one thread at a time.
+   */
+  final def write(newX: Int, newY: Int): Unit =
+    setGeneration(generation + 1)
+    setX(newX)
+    setY(newY)
+    setGeneration(generation + 1)
+
+  /** Copy the values previously written into this data structure into a tuple.
+   *  This method is always safe to call.
+   *  The implementation of this method is not allowed to call `synchronized`.
+   */
+  final def copy(): (Int, Int) = ???
+
+end SeqCount
diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/AtomicVariable.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/AtomicVariable.scala
new file mode 100644
index 0000000000000000000000000000000000000000..5a5d2f4a94ad73544c265ee86d80085fcb5d48cb
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/AtomicVariable.scala
@@ -0,0 +1,28 @@
+package m21.instrumentation
+
+import java.util.concurrent.atomic._
+
+abstract class AbstractAtomicVariable[T] {
+  def get: T
+  def set(value: T): Unit
+  def compareAndSet(expect: T, newval: T) : Boolean
+}
+
+class AtomicVariable[T](initial: T) extends AbstractAtomicVariable[T] {
+
+  private val atomic = new AtomicReference[T](initial)
+
+  override def get: T = atomic.get()
+
+  override def set(value: T): Unit = atomic.set(value)
+
+  override def compareAndSet(expected: T, newValue: T): Boolean = {
+    val current = atomic.get
+    if (current == expected) {
+      atomic.compareAndSet(current, newValue)
+    }
+    else {
+      false
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/Monitor.scala b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/Monitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..06551000717e74916191ea0ed606405390439aca
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/instrumentation/Monitor.scala
@@ -0,0 +1,23 @@
+package m21.instrumentation
+
+class Dummy
+
+trait Monitor {
+  implicit val dummy: Dummy = new Dummy
+
+  def wait()(implicit i: Dummy) = waitDefault()
+
+  def synchronized[T](e: => T)(implicit i: Dummy) = synchronizedDefault(e)
+
+  def notify()(implicit i: Dummy) = notifyDefault()
+
+  def notifyAll()(implicit i: Dummy) = notifyAllDefault()
+
+  private val lock = new AnyRef
+
+  // Can be overriden.
+  def waitDefault(): Unit = lock.wait()
+  def synchronizedDefault[T](toExecute: =>T): T = lock.synchronized(toExecute)
+  def notifyDefault(): Unit = lock.notify()
+  def notifyAllDefault(): Unit = lock.notifyAll()
+}
diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/TestSuite.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/TestSuite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..5038092c46f6020e81887598e6e30d0f6b5d18b2
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/TestSuite.scala
@@ -0,0 +1,122 @@
+package m21
+
+import scala.concurrent._
+import scala.concurrent.duration._
+import scala.collection.mutable.HashMap
+import scala.util.Random
+import instrumentation._
+import instrumentation.TestHelper._
+import instrumentation.TestUtils._
+
+enum ThreadResult:
+  case WriteError(error: String)
+  case WriteSuccess
+  case Read(result: (Int, Int))
+import ThreadResult._
+
+class M21Suite extends munit.FunSuite:
+  /** If at least one thread resulted in an error,
+   *  return `(false, errorMessage)` otherwise return `(true, "")`.
+   */
+  def processResults(results: List[ThreadResult]): (Boolean, String) =
+    val success = (true, "")
+    results.foldLeft(success) {
+      case (acc @ (false, _), _) =>
+        // Report the first error found
+        acc
+      case (_, WriteError(error)) =>
+        (false, error)
+      case (_, Read((x, y))) if x + 1 != y =>
+        (false, s"Read ($x, $y) but expected y to be ${x + 1}")
+      case (_, _: Read | WriteSuccess) =>
+        success
+    }
+
+  def randomList(length: Int): List[Int] =
+    List.fill(length)(Random.nextInt)
+
+  test("SeqCount: single-threaded write and copy (1 pts)") {
+    val sc = new SeqCount
+    randomList(100).lazyZip(randomList(100)).foreach { (x, y) =>
+      sc.write(x, y)
+      assertEquals(sc.copy(), (x, y))
+    }
+  }
+
+  test("SeqCount: one write thread, two copy threads (4 pts)") {
+    testManySchedules(3, sched =>
+      val sc = new SchedulableSeqCount(sched)
+      // Invariant in this test: y == x + 1
+      sc.write(0, 1)
+
+      val randomValues = randomList(length = 5)
+
+      def writeThread(): ThreadResult =
+        randomValues.foldLeft(WriteSuccess) {
+          case (res: WriteError, _) =>
+            // Report the first error found
+            res
+          case (_, i) =>
+            sc.write(i, i + 1)
+            val writtenValues = (i, i + 1)
+            val readBack = sc.copy()
+            if writtenValues != readBack then
+              WriteError(s"Wrote $writtenValues but read back $readBack")
+            else
+              WriteSuccess
+        }
+
+      def copyThread(): ThreadResult =
+        Read(sc.copy())
+
+      val threads = List(
+        () => writeThread(),
+        () => copyThread(),
+        () => copyThread()
+      )
+
+      (threads, results => processResults(results.asInstanceOf[List[ThreadResult]]))
+    )
+  }
+
+  test("MultiWriterSeqCount: single-threaded write and copy (1 pts)") {
+    val sc = new MultiWriterSeqCount
+    randomList(100).lazyZip(randomList(100)).foreach { (x, y) =>
+      sc.write(x, y)
+      assertEquals(sc.copy(), (x, y))
+    }
+  }
+
+  test("MultiWriterSeqCount: two write threads, two copy threads (4 pts)") {
+    testManySchedules(4, sched =>
+      val msc = new SchedulableMultiWriterSeqCount(sched)
+      // Invariant in this test: y == x + 1
+      msc.write(0, 1)
+
+      val randomValues = randomList(length = 5)
+
+      def writeThread(): ThreadResult =
+        randomValues.foreach(i => msc.write(i, i + 1))
+        // Unlke in the SeqCount test, we do not verify that we can read back
+        // the values we wrote, because the other writer thread might have
+        // overwritten them already.
+        WriteSuccess
+
+      def copyThread(): ThreadResult =
+        Read(msc.copy())
+
+      val threads = List(
+        () => writeThread(),
+        () => writeThread(),
+        () => copyThread(),
+        () => copyThread()
+      )
+
+      (threads, results => processResults(results.asInstanceOf[List[ThreadResult]]))
+    )
+  }
+
+  import scala.concurrent.duration._
+  override val munitTimeout = 200.seconds
+end M21Suite
+
diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/MockedMonitor.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/MockedMonitor.scala
new file mode 100644
index 0000000000000000000000000000000000000000..f7629e1b14693b723511bb67212fcdb16c64421c
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/MockedMonitor.scala
@@ -0,0 +1,72 @@
+package m21.instrumentation
+
+trait MockedMonitor extends Monitor {
+  def scheduler: Scheduler
+  
+  // Can be overriden.
+  override def waitDefault() = {
+    scheduler.log("wait")
+    scheduler updateThreadState Wait(this, scheduler.threadLocks.tail)
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    scheduler.log("synchronized check") 
+    val prevLocks = scheduler.threadLocks
+    scheduler updateThreadState Sync(this, prevLocks) // If this belongs to prevLocks, should just continue.
+    scheduler.log("synchronized -> enter")
+    try {
+      toExecute
+    } finally {
+      scheduler updateThreadState Running(prevLocks)
+      scheduler.log("synchronized -> out")
+    }    
+  }
+  override def notifyDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => SyncUnique(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notify")
+  }
+  override def notifyAllDefault() = {
+    scheduler mapOtherStates {
+      state => state match {
+        case Wait(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case SyncUnique(lockToAquire, locks) if lockToAquire == this => Sync(this, state.locks)
+        case e => e
+      }
+    }
+    scheduler.log("notifyAll")
+  }
+}
+
+trait LockFreeMonitor extends Monitor {
+  override def waitDefault() = {
+    throw new Exception("Please use lock-free structures and do not use wait()")
+  }
+  override def synchronizedDefault[T](toExecute: =>T): T = {
+    throw new Exception("Please use lock-free structures and do not use synchronized()")
+  }
+  override def notifyDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notify()")
+  }
+  override def notifyAllDefault() = {
+    throw new Exception("Please use lock-free structures and do not use notifyAll()")
+  }
+}
+
+
+abstract class ThreadState {
+  def locks: Seq[AnyRef]
+}
+trait CanContinueIfAcquiresLock extends ThreadState {
+  def lockToAquire: AnyRef
+}
+case object Start extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case object End extends ThreadState { def locks: Seq[AnyRef] = Seq.empty }
+case class Wait(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState
+case class SyncUnique(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Sync(lockToAquire: AnyRef, locks: Seq[AnyRef]) extends ThreadState with CanContinueIfAcquiresLock
+case class Running(locks: Seq[AnyRef]) extends ThreadState
+case class VariableReadWrite(locks: Seq[AnyRef]) extends ThreadState
diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Scheduler.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Scheduler.scala
new file mode 100644
index 0000000000000000000000000000000000000000..cef9ac5155bd3c6747aea2f7c4d85e3616e84fbb
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Scheduler.scala
@@ -0,0 +1,304 @@
+package m21.instrumentation
+
+import java.util.concurrent._;
+import scala.concurrent.duration._
+import scala.collection.mutable._
+import Stats._
+
+import java.util.concurrent.atomic.AtomicInteger
+
+sealed abstract class Result
+case class RetVal(rets: List[Any]) extends Result
+case class Except(msg: String, stackTrace: Array[StackTraceElement]) extends Result
+case class Timeout(msg: String) extends Result
+
+/**
+ * A class that maintains schedule and a set of thread ids.
+ * The schedules are advanced after an operation of a SchedulableBuffer is performed.
+ * Note: the real schedule that is executed may deviate from the input schedule
+ * due to the adjustments that had to be made for locks
+ */
+class Scheduler(sched: List[Int]) {
+  val maxOps = 500 // a limit on the maximum number of operations the code is allowed to perform
+
+  private var schedule = sched
+  private var numThreads = 0
+  private val realToFakeThreadId = Map[Long, Int]()
+  private val opLog = ListBuffer[String]() // a mutable list (used for efficient concat)
+  private val threadStates = Map[Int, ThreadState]()
+
+  /**
+   * Runs a set of operations in parallel as per the schedule.
+   * Each operation may consist of many primitive operations like reads or writes
+   * to shared data structure each of which should be executed using the function `exec`.
+   * @timeout in milliseconds
+   * @return true - all threads completed on time,  false -some tests timed out.
+   */
+  def runInParallel(timeout: Long, ops: List[() => Any]): Result = {
+    numThreads = ops.length
+    val threadRes = Array.fill(numThreads) { None: Any }
+    var exception: Option[Except] = None
+    val syncObject = new Object()
+    var completed = new AtomicInteger(0)
+    // create threads
+    val threads = ops.zipWithIndex.map {
+      case (op, i) =>
+        new Thread(new Runnable() {
+          def run(): Unit = {
+            val fakeId = i + 1
+            setThreadId(fakeId)
+            try {
+              updateThreadState(Start)
+              val res = op()
+              updateThreadState(End)
+              threadRes(i) = res
+              // notify the master thread if all threads have completed
+              if (completed.incrementAndGet() == ops.length) {
+                syncObject.synchronized { syncObject.notifyAll() }
+              }
+            } catch {
+              case e: Throwable if exception != None => // do nothing here and silently fail
+              case e: Throwable =>
+                log(s"throw ${e.toString}")
+                exception = Some(Except(s"Thread $fakeId crashed on the following schedule: \n" + opLog.mkString("\n"),
+                    e.getStackTrace))
+                syncObject.synchronized { syncObject.notifyAll() }
+              //println(s"$fakeId: ${e.toString}")
+              //Runtime.getRuntime().halt(0) //exit the JVM and all running threads (no other way to kill other threads)
+            }
+          }
+        })
+    }
+    // start all threads
+    threads.foreach(_.start())
+    // wait for all threads to complete, or for an exception to be thrown, or for the time out to expire
+    var remTime = timeout
+    syncObject.synchronized {
+      timed { if(completed.get() != ops.length) syncObject.wait(timeout) } { time => remTime -= time }
+    }
+    if (exception.isDefined) {
+      exception.get
+    } else if (remTime <= 1) { // timeout ? using 1 instead of zero to allow for some errors
+      Timeout(opLog.mkString("\n"))
+    } else {
+      // every thing executed normally
+      RetVal(threadRes.toList)
+    }
+  }
+
+  // Updates the state of the current thread
+  def updateThreadState(state: ThreadState): Unit = {
+    val tid = threadId
+    synchronized {
+      threadStates(tid) = state
+    }
+    state match {
+      case Sync(lockToAquire, locks) =>
+        if (locks.indexOf(lockToAquire) < 0) waitForTurn else {
+          // Re-aqcuiring the same lock
+          updateThreadState(Running(lockToAquire +: locks))
+        }
+      case Start      => waitStart()
+      case End        => removeFromSchedule(tid)
+      case Running(_) =>
+      case _          => waitForTurn // Wait, SyncUnique, VariableReadWrite
+    }
+  }
+
+  def waitStart(): Unit = {
+    //while (threadStates.size < numThreads) {
+    //Thread.sleep(1)
+    //}
+    synchronized {
+      if (threadStates.size < numThreads) {
+        wait()
+      } else {
+        notifyAll()
+      }
+    }
+  }
+
+  def threadLocks = {
+    synchronized {
+      threadStates(threadId).locks
+    }
+  }
+
+  def threadState = {
+    synchronized {
+      threadStates(threadId)
+    }
+  }
+
+  def mapOtherStates(f: ThreadState => ThreadState) = {
+    val exception = threadId
+    synchronized {
+      for (k <- threadStates.keys if k != exception) {
+        threadStates(k) = f(threadStates(k))
+      }
+    }
+  }
+
+  def log(str: String) = {
+    if((realToFakeThreadId contains Thread.currentThread().getId())) {
+      val space = (" " * ((threadId - 1) * 2))
+      val s = space + threadId + ":" + "\n".r.replaceAllIn(str, "\n" + space + "  ")
+      opLog += s
+    }
+  }
+
+  /**
+   * Executes a read or write operation to a global data structure as per the given schedule
+   * @param msg a message corresponding to the operation that will be logged
+   */
+  def exec[T](primop: => T)(msg: => String, postMsg: => Option[T => String] = None): T = {
+    if(! (realToFakeThreadId contains Thread.currentThread().getId())) {
+      primop
+    } else {
+      updateThreadState(VariableReadWrite(threadLocks))
+      val m = msg
+      if(m != "") log(m)
+      if (opLog.size > maxOps)
+        throw new Exception(s"Total number of reads/writes performed by threads exceed $maxOps. A possible deadlock!")
+      val res = primop
+      postMsg match {
+        case Some(m) => log(m(res))
+        case None =>
+      }
+      res
+    }
+  }
+
+  private def setThreadId(fakeId: Int) = synchronized {
+    realToFakeThreadId(Thread.currentThread.getId) = fakeId
+  }
+
+  def threadId =
+    try {
+      realToFakeThreadId(Thread.currentThread().getId())
+    } catch {
+    case e: NoSuchElementException =>
+      throw new Exception("You are accessing shared variables in the constructor. This is not allowed. The variables are already initialized!")
+    }
+
+  private def isTurn(tid: Int) = synchronized {
+    (!schedule.isEmpty && schedule.head != tid)
+  }
+
+  def canProceed(): Boolean = {
+    val tid = threadId
+    canContinue match {
+      case Some((i, state)) if i == tid =>
+        //println(s"$tid: Runs ! Was in state $state")
+        canContinue = None
+        state match {
+          case Sync(lockToAquire, locks) => updateThreadState(Running(lockToAquire +: locks))
+          case SyncUnique(lockToAquire, locks) =>
+            mapOtherStates {
+              _ match {
+                case SyncUnique(lockToAquire2, locks2) if lockToAquire2 == lockToAquire => Wait(lockToAquire2, locks2)
+                case e => e
+              }
+            }
+            updateThreadState(Running(lockToAquire +: locks))
+          case VariableReadWrite(locks) => updateThreadState(Running(locks))
+        }
+        true
+      case Some((i, state)) =>
+        //println(s"$tid: not my turn but $i !")
+        false
+      case None =>
+        false
+    }
+  }
+
+  var threadPreference = 0 // In the case the schedule is over, which thread should have the preference to execute.
+
+  /** returns true if the thread can continue to execute, and false otherwise */
+  def decide(): Option[(Int, ThreadState)] = {
+    if (!threadStates.isEmpty) { // The last thread who enters the decision loop takes the decision.
+      //println(s"$threadId: I'm taking a decision")
+      if (threadStates.values.forall { case e: Wait => true case _ => false }) {
+        val waiting = threadStates.keys.map(_.toString).mkString(", ")
+        val s = if (threadStates.size > 1) "s" else ""
+        val are = if (threadStates.size > 1) "are" else "is"
+        throw new Exception(s"Deadlock: Thread$s $waiting $are waiting but all others have ended and cannot notify them.")
+      } else {
+        // Threads can be in Wait, Sync, SyncUnique, and VariableReadWrite mode.
+        // Let's determine which ones can continue.
+        val notFree = threadStates.collect { case (id, state) => state.locks }.flatten.toSet
+        val threadsNotBlocked = threadStates.toSeq.filter {
+          case (id, v: VariableReadWrite)         => true
+          case (id, v: CanContinueIfAcquiresLock) => !notFree(v.lockToAquire) || (v.locks contains v.lockToAquire)
+          case _                                  => false
+        }
+        if (threadsNotBlocked.isEmpty) {
+          val waiting = threadStates.keys.map(_.toString).mkString(", ")
+          val s = if (threadStates.size > 1) "s" else ""
+          val are = if (threadStates.size > 1) "are" else "is"
+          val whoHasLock = threadStates.toSeq.flatMap { case (id, state) => state.locks.map(lock => (lock, id)) }.toMap
+          val reason = threadStates.collect {
+            case (id, state: CanContinueIfAcquiresLock) if !notFree(state.lockToAquire) =>
+              s"Thread $id is waiting on lock ${state.lockToAquire} held by thread ${whoHasLock(state.lockToAquire)}"
+          }.mkString("\n")
+          throw new Exception(s"Deadlock: Thread$s $waiting are interlocked. Indeed:\n$reason")
+        } else if (threadsNotBlocked.size == 1) { // Do not consume the schedule if only one thread can execute.
+          Some(threadsNotBlocked(0))
+        } else {
+          val next = schedule.indexWhere(t => threadsNotBlocked.exists { case (id, state) => id == t })
+          if (next != -1) {
+            //println(s"$threadId: schedule is $schedule, next chosen is ${schedule(next)}")
+            val chosenOne = schedule(next) // TODO: Make schedule a mutable list.
+            schedule = schedule.take(next) ++ schedule.drop(next + 1)
+            Some((chosenOne, threadStates(chosenOne)))
+          } else {
+            threadPreference = (threadPreference + 1) % threadsNotBlocked.size
+            val chosenOne = threadsNotBlocked(threadPreference) // Maybe another strategy
+            Some(chosenOne)
+            //threadsNotBlocked.indexOf(threadId) >= 0
+            /*
+            val tnb = threadsNotBlocked.map(_._1).mkString(",")
+            val s = if (schedule.isEmpty) "empty" else schedule.mkString(",")
+            val only = if (schedule.isEmpty) "" else " only"
+            throw new Exception(s"The schedule is $s but$only threads ${tnb} can continue")*/
+          }
+        }
+      }
+    } else canContinue
+  }
+
+  /**
+   * This will be called before a schedulable operation begins.
+   * This should not use synchronized
+   */
+  var numThreadsWaiting = new AtomicInteger(0)
+  //var waitingForDecision = Map[Int, Option[Int]]() // Mapping from thread ids to a number indicating who is going to make the choice.
+  var canContinue: Option[(Int, ThreadState)] = None // The result of the decision thread Id of the thread authorized to continue.
+  private def waitForTurn = {
+    synchronized {
+      if (numThreadsWaiting.incrementAndGet() == threadStates.size) {
+        canContinue = decide()
+        notifyAll()
+      }
+      //waitingForDecision(threadId) = Some(numThreadsWaiting)
+      //println(s"$threadId Entering waiting with ticket number $numThreadsWaiting/${waitingForDecision.size}")
+      while (!canProceed()) wait()
+    }
+    numThreadsWaiting.decrementAndGet()
+  }
+
+  /**
+   * To be invoked when a thread is about to complete
+   */
+  private def removeFromSchedule(fakeid: Int) = synchronized {
+    //println(s"$fakeid: I'm taking a decision because I finished")
+    schedule = schedule.filterNot(_ == fakeid)
+    threadStates -= fakeid
+    if (numThreadsWaiting.get() == threadStates.size) {
+      canContinue = decide()
+      notifyAll()
+    }
+  }
+
+  def getOperationLog() = opLog
+}
diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Stats.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Stats.scala
new file mode 100644
index 0000000000000000000000000000000000000000..6eb7239c2343a77ed7f9e8193cbacc2d57a16d61
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/Stats.scala
@@ -0,0 +1,23 @@
+/* Copyright 2009-2015 EPFL, Lausanne */
+package m21.instrumentation
+
+import java.lang.management._
+
+/**
+ * A collection of methods that can be used to collect run-time statistics about Leon programs.
+ * This is mostly used to test the resources properties of Leon programs
+ */
+object Stats {
+  def timed[T](code: => T)(cont: Long => Unit): T = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    cont((System.currentTimeMillis() - t1))
+    r
+  }
+
+  def withTime[T](code: => T): (T, Long) = {
+    var t1 = System.currentTimeMillis()
+    val r = code
+    (r, (System.currentTimeMillis() - t1))
+  }
+}
diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestHelper.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestHelper.scala
new file mode 100644
index 0000000000000000000000000000000000000000..5be75d6590a34d6e7410129e66a2f2a3b17e216c
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestHelper.scala
@@ -0,0 +1,124 @@
+package m21.instrumentation
+
+import scala.util.Random
+import scala.collection.mutable.{Map => MutableMap}
+
+import Stats._
+
+object TestHelper {
+  val noOfSchedules = 10000 // set this to 100k during deployment
+  val readWritesPerThread = 20 // maximum number of read/writes possible in one thread
+  val contextSwitchBound = 10
+  val testTimeout = 150 // the total time out for a test in seconds
+  val schedTimeout = 15 // the total time out for execution of a schedule in secs
+
+  // Helpers
+  /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1))
+  def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3))
+  def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/
+
+  def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) =
+    testManySchedules(1,
+      (sched: Scheduler) => {
+        (List(() => ops(sched)),
+         (res: List[Any]) => assertions(res.head.asInstanceOf[T]))
+      })
+
+  /**
+   * @numThreads number of threads
+   * @ops operations to be executed, one per thread
+   * @assertion as condition that will executed after all threads have completed (without exceptions)
+   * 					 the arguments are the results of the threads
+   */
+  def testManySchedules(numThreads: Int,
+      ops: Scheduler =>
+        (List[() => Any], // Threads
+         List[Any] => (Boolean, String)) // Assertion
+      ) = {
+    var timeout = testTimeout * 1000L
+    val threadIds = (1 to numThreads)
+    //(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach {
+    val schedules = (new ScheduleGenerator(numThreads)).schedules()
+    var schedsExplored = 0
+    schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach {
+      //case _ if timeout <= 0 => // break
+      case schedule =>
+        schedsExplored += 1
+        val schedr = new Scheduler(schedule)
+        //println("Exploring Sched: "+schedule)
+        val (threadOps, assertion) = ops(schedr)
+        if (threadOps.size != numThreads)
+          throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps")
+        timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match {
+          case Timeout(msg) =>
+            throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg)
+          case Except(msg, stkTrace) =>
+            val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n")
+            throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr)
+          case RetVal(threadRes) =>
+            // check the assertion
+            val (success, custom_msg) = assertion(threadRes)
+            if (!success) {
+              val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n")
+              throw new java.lang.AssertionError("Assertion failed: "+msg)
+            }
+        }
+    }
+    if (timeout <= 0) {
+      throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!")
+    }
+  }
+
+  /**
+   * A schedule generator that is based on the context bound
+   */
+  class ScheduleGenerator(numThreads: Int) {
+    val scheduleLength = readWritesPerThread * numThreads
+    val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position
+    def schedules(): LazyList[List[Int]] = {
+      var contextSwitches = 0
+      var contexts = List[Int]() // a stack of thread ids in the order of context-switches
+      val remainingOps = MutableMap[Int, Int]()
+      remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread
+      val liveThreads = (1 to numThreads).toSeq.toBuffer
+
+      /**
+       * Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule
+       */
+      def updateState(tid: Int): Unit = {
+        val remOps = remainingOps(tid)
+        if (remOps == 0) {
+          liveThreads -= tid
+        } else {
+          remainingOps += (tid -> (remOps - 1))
+        }
+      }
+      val schedule = rands.foldLeft(List[Int]()) {
+        case (acc, r) if contextSwitches < contextSwitchBound =>
+          val tid = liveThreads(r.nextInt(liveThreads.size))
+          contexts match {
+            case prev :: tail if prev != tid => // we have a new context switch here
+              contexts +:= tid
+              contextSwitches += 1
+            case prev :: tail =>
+            case _ => // init case
+              contexts +:= tid
+          }
+          updateState(tid)
+          acc :+ tid
+        case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches
+          if (!contexts.isEmpty) {
+            contexts = contexts.dropWhile(remainingOps(_) == 0)
+          }
+          val tid = contexts match {
+            case top :: tail => top
+            case _ => liveThreads(0)  // here, there has to be threads that have not even started
+          }
+          updateState(tid)
+          acc :+ tid
+      }
+      schedule #:: schedules()
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestUtils.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestUtils.scala
new file mode 100644
index 0000000000000000000000000000000000000000..5da760477971e5faad31593cd61552ec2e7b4ba6
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/instrumentation/TestUtils.scala
@@ -0,0 +1,19 @@
+package m21.instrumentation
+
+import scala.concurrent._
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object TestUtils {
+  def failsOrTimesOut[T](action: => T): Boolean = {
+    val asyncAction = Future {
+      action
+    }
+    try {
+      Await.result(asyncAction, 2000.millisecond)
+    } catch {
+      case _: Throwable => return true
+    }
+    return false
+  }
+}
diff --git a/previous-exams/2021-midterm/m21/src/test/scala/m21/overrides.scala b/previous-exams/2021-midterm/m21/src/test/scala/m21/overrides.scala
new file mode 100644
index 0000000000000000000000000000000000000000..92d0cec220a473d6e112d44ca7456037d08503b6
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/test/scala/m21/overrides.scala
@@ -0,0 +1,68 @@
+package m21
+
+import instrumentation._
+
+import scala.annotation.tailrec
+import java.util.concurrent.atomic._
+
+class SchedulableAtomicVariable[T](initial: T, scheduler: Scheduler, name: String) extends AbstractAtomicVariable[T]:
+  private val proxied: AtomicVariable[T] = new AtomicVariable[T](initial)
+
+  override def get: T = scheduler.exec {
+    proxied.get
+  } (s"", Some(res => s"$name: get $res"))
+
+  override def set(value: T): Unit = scheduler.exec {
+    proxied.set(value)
+  } (s"$name: set $value", None)
+
+  override def compareAndSet(expected: T, newValue: T): Boolean = {
+    scheduler.exec {
+      proxied.compareAndSet(expected, newValue)
+    } (s"$name: compareAndSet(expected = $expected, newValue = $newValue)", Some(res => s"$name: Did it set? $res") )
+  }
+
+end SchedulableAtomicVariable
+
+class SchedulableSeqCount(val scheduler: Scheduler) extends SeqCount with LockFreeMonitor:
+  override def generation: Int = scheduler.exec {
+    super.generation
+  } ("", Some(res => s"generation is $res"))
+  override def setGeneration(newGeneration: Int): Unit = scheduler.exec {
+    super.setGeneration(newGeneration)
+  } ( s"setGeneration($newGeneration)", None )
+
+  override def x: Int = scheduler.exec {
+    super.x
+  } ("", Some(res => s"x is $res"))
+  override def setX(newX: Int): Unit = scheduler.exec {
+    super.setX(newX)
+  } (s"setX($newX)", None)
+
+  override def y: Int = scheduler.exec {
+    super.y
+  } ("", Some(res => s"y is $res"))
+  override def setY(newY: Int): Unit = scheduler.exec {
+    super.setY(newY)
+  } (s"setY($newY)", None)
+
+end SchedulableSeqCount
+
+class SchedulableMultiWriterSeqCount(val scheduler: Scheduler) extends MultiWriterSeqCount with LockFreeMonitor:
+  override protected val myGeneration: AbstractAtomicVariable[Int] = new SchedulableAtomicVariable(1, scheduler, "myGeneration")
+
+  override def x: Int = scheduler.exec {
+    super.x
+  } ("", Some(res => s"x is $res"))
+  override def setX(newX: Int): Unit = scheduler.exec {
+    super.setX(newX)
+  } (s"setX($newX)", None)
+
+  override def y: Int = scheduler.exec {
+    super.y
+  } ("", Some(res => s"y is $res"))
+  override def setY(newY: Int): Unit = scheduler.exec {
+    super.setY(newY)
+  } (s"setY($newY)", None)
+
+end SchedulableMultiWriterSeqCount
diff --git a/previous-exams/2021-midterm-solutions/m3.md b/previous-exams/2021-midterm/m3.md
similarity index 94%
rename from previous-exams/2021-midterm-solutions/m3.md
rename to previous-exams/2021-midterm/m3.md
index ee3969ef97a4ed980c3d1cbbcfd1b60ced495a34..88365dc2670fdade4186d1b52204daea41bd9609 100644
--- a/previous-exams/2021-midterm-solutions/m3.md
+++ b/previous-exams/2021-midterm/m3.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m3 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m3
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m3/.gitignore b/previous-exams/2021-midterm/m3/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m3/assignment.sbt b/previous-exams/2021-midterm/m3/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m3/build.sbt b/previous-exams/2021-midterm/m3/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..b15645acb40417c7326345ed22ac4115e6e88735
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m3"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m3.M3Suite"
diff --git a/previous-exams/2021-midterm/m3/grading-tests.jar b/previous-exams/2021-midterm/m3/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..01481c432fa1fc85da0c4c660daebb5f6fffd40d
Binary files /dev/null and b/previous-exams/2021-midterm/m3/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m3/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m3/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m3/project/MOOCSettings.scala b/previous-exams/2021-midterm/m3/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m3/project/StudentTasks.scala b/previous-exams/2021-midterm/m3/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m3/project/build.properties b/previous-exams/2021-midterm/m3/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m3/project/buildSettings.sbt b/previous-exams/2021-midterm/m3/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m3/project/plugins.sbt b/previous-exams/2021-midterm/m3/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m3/src/main/scala/m3/Lib.scala b/previous-exams/2021-midterm/m3/src/main/scala/m3/Lib.scala
new file mode 100644
index 0000000000000000000000000000000000000000..68e89db2496199c75ec5362e9d9725f3c27facf5
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/src/main/scala/m3/Lib.scala
@@ -0,0 +1,68 @@
+package m3
+
+////////////////////////////////////////
+// NO NEED TO MODIFY THIS SOURCE FILE //
+////////////////////////////////////////
+
+trait Lib {
+
+  /** If an array has `n` elements and `n < THRESHOLD`, then it should be processed sequentially */
+  final val THRESHOLD: Int = 33
+
+  /** Compute the two values in parallel
+   *
+   *  Note: Most tests just compute those two sequentially to make any bug simpler to debug
+   */
+  def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2)
+
+  /** A limited array. It only contains the required operations for this exercise. */
+  trait Arr[T] {
+    /** Get the i-th element of the array (0-based) */
+    def apply(i: Int): T
+    /** Update the i-th element of the array with the given value (0-based) */
+    def update(i: Int, x: T): Unit
+    /** Number of elements in this array */
+    def length: Int
+    /** Create a copy of this array without the first element */
+    def tail: Arr[T]
+    /** Create a copy of this array by mapping all the elements with the given function */
+    def map[U](f: T => U): Arr[U]
+  }
+
+  object Arr {
+    /** Create an array with the given elements */
+    def apply[T](xs: T*): Arr[T] = {
+      val arr: Arr[T] = Arr.ofLength(xs.length)
+      for i <- 0 until xs.length do arr(i) = xs(i)
+      arr
+    }
+
+    /** Create an array with the given length. All elements are initialized to `null`. */
+    def ofLength[T](n: Int): Arr[T] =
+      newArrOfLength(n)
+
+  }
+
+  /** Create an array with the given length. All elements are initialized to `null`. */
+  def newArrOfLength[T](n: Int): Arr[T]
+
+  /** A number representing the average of a list of integers (the "window") */
+  case class AvgWin(list: List[Int]) {
+    def push(i: Int) = list match {
+      case i3 :: i2 :: i1 :: Nil => AvgWin(i :: i3 :: i2 :: Nil)
+      case list => AvgWin(i :: list)
+    }
+
+    def pushAll(other: AvgWin) =
+      other.list.foldRight(this)((el, self) => self.push(el))
+
+    def toDouble: Double = if list.isEmpty then 0 else list.sum / list.length
+  }
+
+  /** Tree result of an upsweep operation. Specialized for `AvgWin` results. */
+  trait TreeRes { val res: AvgWin }
+  /** Leaf result of an upsweep operation. Specialized for `AvgWin` results. */
+  case class Leaf(from: Int, to: Int, res: AvgWin) extends TreeRes
+  /** Tree node result of an upsweep operation. Specialized for `AvgWin` results. */
+  case class Node(left: TreeRes, res: AvgWin, right: TreeRes) extends TreeRes
+}
diff --git a/previous-exams/2021-midterm/m3/src/main/scala/m3/M3.scala b/previous-exams/2021-midterm/m3/src/main/scala/m3/M3.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c9b722da8dcb2ec0d381d49793b6bbcb3d0daf32
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/src/main/scala/m3/M3.scala
@@ -0,0 +1,89 @@
+package m3
+
+
+trait M3 extends Lib {
+  // Functions and classes of Lib can be used in here
+
+  /** Compute the rolling windowed mean of an array.
+   *
+   *  For an array `arr = Arr(x1, x2, x3, ..., x_n)` the result is
+   *  `Arr(x1, (x1+x2)/2, (x1+x2+x3)/3, (x2+x3+x4)/3, ..., (x_{n-2}, x_{n-1}, x_n)/3)`
+   */
+  def rollingWinMeanParallel(arr: Arr[Int]): Arr[Double] = {
+    if (arr.length == 0) return Arr.ofLength(0)
+    // TASK 1:  Add missing parallelization in `upsweep` and `downsweep`.
+    //          You should use the `parallel` method.
+    //          You should use the sequential version if the number of elements is lower than THRESHOLD.
+    // TASK 2a: Pass `arr` to `upsweep` and `downsweep` instead of `tmp`.
+    //          You will need to change some signatures and update the code appropriately.
+    //          Remove the definition of `tmp`
+    // TASK 2b: Change the type of the array `out` from `AvgWin` to `Double`
+    //          You will need to change some signatures and update the code appropriately.
+    //          Remove the call `.map(root => root.toDouble)`.
+    // TASK 3:  Remove the call to `.tail`.
+    //          Update the update the code appropriately.
+
+    val tmp: Arr[AvgWin] = arr.map(x => AvgWin(x :: Nil))
+    val out: Arr[AvgWin] = Arr.ofLength(arr.length + 1)
+    val tree = upsweep(tmp, 0, arr.length)
+    downsweep(tmp, AvgWin(Nil), tree, out)
+    out(0) = AvgWin(Nil)
+    out.map(root => root.toDouble).tail
+
+    // IDEAL SOLUTION
+    // val out = Arr.ofLength(arr.length)
+    // val tree = upsweep(arr, 0, arr.length)
+    // downsweep(arr, AvgWin(Nil), tree, out)
+    // out
+  }
+
+  def scanOp(acc: AvgWin, x: AvgWin) = // No need to modify this method
+    acc.pushAll(x)
+
+  def upsweep(input: Arr[AvgWin], from: Int, to: Int): TreeRes = {
+    if (to - from < 2)
+      Leaf(from, to, reduceSequential(input, from + 1, to, input(from)))
+    else {
+      val mid = from + (to - from) / 2
+      val (tL, tR) = (
+        upsweep(input, from, mid),
+        upsweep(input, mid, to)
+      )
+      Node(tL, scanOp(tL.res, tR.res), tR)
+    }
+  }
+
+  def downsweep(input: Arr[AvgWin], a0: AvgWin, tree: TreeRes, output: Arr[AvgWin]): Unit = {
+    tree match {
+      case Node(left, _, right) =>
+        (
+          downsweep(input, a0, left, output),
+          downsweep(input, scanOp(a0, left.res), right, output)
+        )
+      case Leaf(from, to, _) =>
+        downsweepSequential(input, from, to, a0, output)
+    }
+  }
+
+  def downsweepSequential(input: Arr[AvgWin], from: Int, to: Int, a0: AvgWin, output: Arr[AvgWin]): Unit = {
+    if (from < to) {
+      var i = from
+      var a = a0
+      while (i < to) {
+        a = scanOp(a, input(i))
+        i = i + 1
+        output(i) = a
+      }
+    }
+  }
+
+  def reduceSequential(input: Arr[AvgWin], from: Int, to: Int, a0: AvgWin): AvgWin = {
+    var a = a0
+    var i = from
+    while (i < to) {
+      a = scanOp(a, input(i))
+      i = i + 1
+    }
+    a
+  }
+}
diff --git a/previous-exams/2021-midterm/m3/src/test/scala/m3/M3Suite.scala b/previous-exams/2021-midterm/m3/src/test/scala/m3/M3Suite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..176df6a14e456a6bfa29e250d20aa94253c2360b
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/src/test/scala/m3/M3Suite.scala
@@ -0,0 +1,174 @@
+package m3
+
+class M3Suite extends munit.FunSuite {
+
+  test("Rolling windowed average result test (5pts)") {
+    RollingWinMeanBasicLogicTest.basicTests()
+    RollingWinMeanBasicLogicTest.normalTests()
+    RollingWinMeanBasicLogicTest.largeTests()
+  }
+
+  test("[TASK 1] Rolling windowed average parallelism test (30pts)") {
+    RollingWinMeanCallsToParallel.parallelismTest()
+    RollingWinMeanParallel.basicTests()
+    RollingWinMeanParallel.normalTests()
+    RollingWinMeanParallel.largeTests()
+  }
+
+  test("[TASK 2] Rolling windowed average no `map` test (35pts)") {
+    RollingWinMeanNoMap.basicTests()
+    RollingWinMeanNoMap.normalTests()
+    RollingWinMeanNoMap.largeTests()
+  }
+
+  test("[TASK 3] Rolling windowed average no `tail` test (30pts)") {
+    RollingWinMeanNoTail.basicTests()
+    RollingWinMeanNoTail.normalTests()
+    RollingWinMeanNoTail.largeTests()
+  }
+
+
+  object RollingWinMeanBasicLogicTest extends M3 with LibImpl with RollingWinMeanTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+  }
+
+  object RollingWinMeanCallsToParallel extends M3 with LibImpl with RollingWinMeanTest {
+    private var count = 0
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) =
+      count += 1
+      (op1, op2)
+
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+
+    def parallelismTest() = {
+      assertParallelCount(Arr(), 0)
+      assertParallelCount(Arr(1), 0)
+      assertParallelCount(Arr(1, 2, 3, 4), 0)
+      assertParallelCount(Arr(Array.tabulate(16)(identity): _*), 0)
+      assertParallelCount(Arr(Array.tabulate(32)(identity): _*), 0)
+
+      assertParallelCount(Arr(Array.tabulate(33)(identity): _*), 2)
+      assertParallelCount(Arr(Array.tabulate(64)(identity): _*), 2)
+      assertParallelCount(Arr(Array.tabulate(128)(identity): _*), 6)
+      assertParallelCount(Arr(Array.tabulate(256)(identity): _*), 14)
+      assertParallelCount(Arr(Array.tabulate(1000)(identity): _*), 62)
+      assertParallelCount(Arr(Array.tabulate(1024)(identity): _*), 62)
+    }
+
+    def assertParallelCount(arr: Arr[Int], expected: Int): Unit = {
+      try {
+        count = 0
+        rollingWinMeanParallel(arr)
+        assert(count == expected, {
+          val extra = if (expected == 0) "" else s" ${expected/2} for the `upsweep` and ${expected/2} for the `downsweep`"
+          s"\n$arr\n\nERROR: Expected $expected instead of $count calls to `parallel(...)` for an array of ${arr.length} elements. Current parallel threshold is $THRESHOLD.$extra"
+        })
+      } finally {
+        count = 0
+      }
+    }
+
+  }
+
+  object RollingWinMeanNoMap extends M3 with LibImpl with RollingWinMeanTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) {
+      override def map[U](f: T => U): Arr[U] = throw Exception("Should not call Arr.map")
+    }
+  }
+
+  object RollingWinMeanNoTail extends M3 with LibImpl with RollingWinMeanTest {
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = (op1, op2)
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl[T](arr) {
+      override def tail: Arr[T] = throw Exception("Should not call Arr.tail")
+    }
+  }
+
+  object RollingWinMeanParallel extends M3 with LibImpl with RollingWinMeanTest {
+    import scala.concurrent.duration._
+    val TIMEOUT = Duration(10, SECONDS)
+    def parallel[T1, T2](op1: => T1, op2: => T2): (T1, T2) = {
+      import concurrent.ExecutionContext.Implicits.global
+      import scala.concurrent._
+      Await.result(Future(op1).zip(Future(op2)), TIMEOUT) // FIXME not timing-out
+    }
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T] = new ArrImpl(arr)
+  }
+
+  trait LibImpl extends Lib {
+
+    def newArrFrom[T](arr: Array[AnyRef]): Arr[T]
+
+    def newArrOfLength[T](n: Int): Arr[T] =
+      newArrFrom(new Array(n))
+
+    class ArrImpl[T](val arr: Array[AnyRef]) extends Arr[T]:
+      def apply(i: Int): T =
+        arr(i).asInstanceOf[T]
+      def update(i: Int, x: T): Unit =
+        arr(i) = x.asInstanceOf[AnyRef]
+      def length: Int =
+        arr.length
+      def map[U](f: T => U): Arr[U] =
+        newArrFrom(arr.map(f.asInstanceOf[AnyRef => AnyRef]))
+      def tail: Arr[T] =
+        newArrFrom(arr.tail)
+      override def toString: String =
+        arr.mkString("Arr(", ", ", ")")
+      override def equals(that: Any): Boolean =
+        that match
+          case that: ArrImpl[_] => Array.equals(arr, that.arr)
+          case _ => false
+  }
+
+  trait RollingWinMeanTest extends M3 {
+
+    def tabulate[T](n: Int)(f: Int => T): Arr[T] =
+      val arr = Arr.ofLength[T](n)
+      for i <- 0 until n do
+        arr(i) = f(i)
+      arr
+
+    def asSeq(arr: Arr[Double]) =
+      val array = new Array[Double](arr.length)
+      for i <- 0 to (arr.length - 1) do
+        array(i) = arr(i)
+      array.toSeq
+
+    def scanOp_(acc: AvgWin, x: AvgWin) =
+      acc.pushAll(x)
+
+    def result(ds: Seq[Int]): Arr[Double] =
+      Arr(ds.map(x => AvgWin(x :: Nil)).scan(AvgWin(Nil))(scanOp_).tail.map(_.toDouble): _*)
+
+    def check(input: Seq[Int]) =
+      assertEquals(
+        asSeq(rollingWinMeanParallel(Arr(input: _*))),
+        asSeq(result(input))
+      )
+
+    def basicTests() = {
+      check(Seq())
+      check(Seq(1))
+      check(Seq(1, 2, 3, 4))
+      check(Seq(4, 4, 4, 4))
+    }
+
+    def normalTests() = {
+      check(Seq.tabulate(64)(identity))
+      check(Seq(4, 4, 4, 4))
+      check(Seq(4, 8, 6, 4))
+      check(Seq(4, 3, 2, 1))
+      check(Seq.tabulate(64)(identity).reverse)
+      check(Seq.tabulate(128)(i => 128 - 2*i).reverse)
+    }
+
+    def largeTests() = {
+      check(Seq.tabulate(500)(identity))
+      check(Seq.tabulate(512)(identity))
+      check(Seq.tabulate(1_000)(identity))
+      check(Seq.tabulate(10_000)(identity))
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm-solutions/m6.md b/previous-exams/2021-midterm/m6.md
similarity index 97%
rename from previous-exams/2021-midterm-solutions/m6.md
rename to previous-exams/2021-midterm/m6.md
index f0eb44964732a65fc946f8e095ad1948048799ac..6280bbb4202df0f0595b58e76a31a4182b96a6c4 100644
--- a/previous-exams/2021-midterm-solutions/m6.md
+++ b/previous-exams/2021-midterm/m6.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m6 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m6
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m6/.gitignore b/previous-exams/2021-midterm/m6/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m6/assignment.sbt b/previous-exams/2021-midterm/m6/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m6/build.sbt b/previous-exams/2021-midterm/m6/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..96606f6e8cb06d2e80e604e6d720bfbd3a83819c
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m6"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m6.M6Suite"
diff --git a/previous-exams/2021-midterm/m6/grading-tests.jar b/previous-exams/2021-midterm/m6/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..cdd54531b11cad5a6a0cb6060a4495ca86da20c0
Binary files /dev/null and b/previous-exams/2021-midterm/m6/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m6/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m6/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m6/project/MOOCSettings.scala b/previous-exams/2021-midterm/m6/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m6/project/StudentTasks.scala b/previous-exams/2021-midterm/m6/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m6/project/build.properties b/previous-exams/2021-midterm/m6/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m6/project/buildSettings.sbt b/previous-exams/2021-midterm/m6/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m6/project/plugins.sbt b/previous-exams/2021-midterm/m6/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m6/src/main/scala/m6/M6.scala b/previous-exams/2021-midterm/m6/src/main/scala/m6/M6.scala
new file mode 100644
index 0000000000000000000000000000000000000000..4e54913310d29d409c3bd5bb3512f2634f8297be
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/src/main/scala/m6/M6.scala
@@ -0,0 +1,69 @@
+package m6
+
+import java.util.concurrent._
+import scala.util.DynamicVariable
+
+trait M6 extends Lib {
+
+  class DLLCombinerImplementation(chunk_size: Int = 3) extends DLLCombiner(chunk_size){
+
+    // Computes every other Integer element of data array, starting from the first (index 0), up to the middle
+    def task1(data: Array[Int]): ForkJoinTask[Unit] = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the second, up to the middle
+    def task2(data: Array[Int]): ForkJoinTask[Unit] = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the second to last, up to the middle
+    def task3(data: Array[Int]): ForkJoinTask[Unit] = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the last, up to the middle
+    // This is executed on the current thread.
+    def task4(data: Array[Int]): Unit = {
+      ???
+    }
+
+    def result(): Array[Int] = {
+      val data = new Array[Int](cnt)
+
+      ???
+
+      data
+    }
+
+    private def copyForward(data: Array[Int], curr: Node, from: Int, to: Int, limit: Int) = {
+      var current = curr
+      var i_from = from
+      var i_to = to 
+
+      while (i_to < limit) {
+        try {
+          data(i_to) = current.value(i_from)
+          i_to += 2
+          i_from += 2
+          if(i_from == current.cnt){
+            current = current.next
+            i_from = 0
+          }
+          else if(i_from > current.cnt) {
+            current = current.next
+            i_from = 1
+            if(current.cnt == 1) {
+              current = current.next
+              i_from = 0
+            }
+          }
+        }
+        catch{
+          case e: Exception =>
+        }
+      }
+    }
+  }
+
+}
diff --git a/previous-exams/2021-midterm/m6/src/main/scala/m6/lib.scala b/previous-exams/2021-midterm/m6/src/main/scala/m6/lib.scala
new file mode 100644
index 0000000000000000000000000000000000000000..bfb28387fa6826b708bc35f3aa47417be7064840
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/src/main/scala/m6/lib.scala
@@ -0,0 +1,84 @@
+package m6
+
+import java.util.concurrent._
+import scala.util.DynamicVariable
+
+trait Lib {
+
+  class Node(val size: Int) {
+    var value: Array[Int] = new Array(size)
+    var next: Node = null    
+    var previous: Node = null
+    var cnt = 0
+
+    def add(v: Int) = {
+      value(cnt) = v
+      cnt += 1
+    }
+  }
+
+  // Simplified Combiner interface
+  // Implements methods += and combine
+  // Abstract methods should be implemented in subclasses
+  abstract class DLLCombiner(val chunk_size: Int) {
+    var head: Node = null
+    var last: Node = null
+    var cnt: Int = 0
+    var chunks: Int = 0
+
+    // Adds an Integer to the last node of this array combiner. If the last node is full, allocates a new node.
+    def +=(elem: Int): Unit = {
+      if(cnt % chunk_size == 0) {
+        chunks = chunks + 1
+        val node = new Node(chunk_size)
+        if (cnt == 0) {
+          head = node
+          last = node
+        }
+        else {
+          last.next = node
+          node.previous = last
+          last = node
+        }
+      }
+      last.add(elem)
+      cnt += 1
+    }
+
+    // Combines this array combiner and another given combiner in constant O(1) complexity.
+    def combine(that: DLLCombiner): DLLCombiner = {
+      assert(this.chunk_size == that.chunk_size)
+      if (this.cnt == 0) {
+        this.head = that.head
+        this.last = that.last
+        this.cnt = that.cnt
+        this.chunks = that.chunks
+
+        this
+      }
+      else if (that.cnt == 0)
+        this
+      else {
+        this.last.next = that.head
+        that.head.previous = this.last
+
+        this.cnt = this.cnt + that.cnt
+        this.chunks = this.chunks + that.chunks
+        this.last = that.last
+
+        this
+      }
+    }
+
+    def task1(data: Array[Int]): ForkJoinTask[Unit]
+    def task2(data: Array[Int]): ForkJoinTask[Unit]
+    def task3(data: Array[Int]): ForkJoinTask[Unit]
+    def task4(data: Array[Int]): Unit
+
+    def result(): Array[Int]
+
+  }
+
+  def task[T](body: => T): ForkJoinTask[T]
+
+}
\ No newline at end of file
diff --git a/previous-exams/2021-midterm/m6/src/test/scala/m6/M6Suite.scala b/previous-exams/2021-midterm/m6/src/test/scala/m6/M6Suite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..e04fcf064cb9f9c7262b90f94365cd4651fa49d0
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/src/test/scala/m6/M6Suite.scala
@@ -0,0 +1,422 @@
+package m6
+
+import java.util.concurrent._
+import scala.util.DynamicVariable
+
+class M6Suite extends AbstractM6Suite {
+
+  // (70+) 30 / 500 points for correct implementation, don't check parallelism
+  test("[Correctness] fetch result - simple combiners (5pts)") {
+    assertCorrectnessSimple()
+  }
+
+  test("[Correctness] fetch result - small combiners (5pts)") {
+    assertCorrectnessBasic()
+  }
+
+  test("[Correctness] fetch result - small combiners after combining (10pts)") {
+    assertCorrectnessCombined()
+  }
+
+  test("[Correctness] fetch result - large combiners (10pts)") {
+    assertCorrectnessLarge()
+  }
+
+  def assertCorrectnessSimple() = {
+    simpleCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  def assertCorrectnessBasic() = {
+    basicCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  def assertCorrectnessCombined() = {
+    combinedCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  def assertCorrectnessLarge() = {
+    largeCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  // (70+30+) 50 / 500 points for correct parallel implementation, don't check if it's exactly 1/4 of the array per task
+  private var count = 0
+  private val expected = 3
+
+  override def task[T](body: => T): ForkJoinTask[T] = {
+    count += 1
+    scheduler.value.schedule(body)
+  }
+
+  test("[TaskCount] number of newly created tasks should be 3 (5pts)") {
+    assertTaskCountSimple()
+  }
+
+  test("[TaskCount] fetch result and check parallel - simple combiners (5pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessSimple()
+  }
+
+  test("[TaskCount] fetch result and check parallel - small combiners (10pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessBasic()
+  }
+
+  test("[TaskCount] fetch result and check parallel - small combiners after combining (15pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessCombined()
+  }
+
+  test("[TaskCount] fetch result and check parallel - large combiners (15pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessLarge()
+  }
+
+  def assertTaskCountSimple(): Unit = {
+    simpleCombiners.foreach(elem => assertTaskCount(elem._1, elem._2))
+  }
+
+  def assertTaskCount(combiner: DLLCombinerTest, array: Array[Int]): Unit = {
+    try {
+      count = 0
+      build(combiner, array)
+      combiner.result()
+      assertEquals(count, expected, {
+        s"ERROR: Expected $expected instead of $count calls to `task(...)`"
+      })
+    } finally {
+      count = 0
+    }
+  }
+
+  //(70+30+50+) 200 / 500 points for correct parallel implementation, exactly 1/4 of the array per task
+  test("[TaskFairness] each task should compute 1/4 of the result (50pts)") {
+    assertTaskFairness(simpleCombiners.unzip._1)
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - simple combiners (20pts)") {
+    assertTaskFairness(simpleCombiners.unzip._1)
+    assertCorrectnessSimple()
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners (30pts)") {
+    assertTaskFairness(basicCombiners.unzip._1)
+    assertCorrectnessBasic()
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners after combining (50pts)") {
+    assertTaskFairness(combinedCombiners.unzip._1)
+    assertCorrectnessCombined()
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - large combiners (50pts)") {
+    assertTaskFairness(largeCombiners.unzip._1)
+    assertCorrectnessLarge()
+  }
+
+  def assertTaskFairness(combiners: List[DLLCombinerTest]): Unit = {
+    def assertNewTaskFairness(combiner: DLLCombinerTest, task: ForkJoinTask[Unit], data: Array[Int]) = {
+      var count = 0
+      var expected = combiner.cnt / 4
+      task.join
+      count = data.count(elem => elem != 0)
+
+      assert((count - expected).abs <= 1)
+    }
+
+    def assertMainTaskFairness(combiner: DLLCombinerTest, task: Unit, data: Array[Int]) = {
+      var count = 0
+      var expected = combiner.cnt / 4
+      count = data.count(elem => elem != 0)
+
+      assert((count - expected).abs <= 1)
+    }
+
+    combiners.foreach { elem =>
+      var data = Array.fill(elem.cnt)(0)
+      assertNewTaskFairness(elem, elem.task1(data), data)
+
+      data = Array.fill(elem.cnt)(0)
+      assertNewTaskFairness(elem, elem.task2(data), data)
+
+      data = Array.fill(elem.cnt)(0)
+      assertNewTaskFairness(elem, elem.task3(data), data)
+
+      data = Array.fill(elem.cnt)(0)
+      assertMainTaskFairness(elem, elem.task4(data), data)
+    }
+  }
+
+  //(70+30+50+200+) 150 / 500 points for correct parallel implementation, exactly 1/4 of the array per task, exactly the specified quarter
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - simple combiners (20pts)") {
+    assertTaskPrecision(simpleCombiners)
+  }
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners (30pts)") {
+    assertTaskPrecision(basicCombiners)
+  }
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners after combining (50pts)") {
+    assertTaskPrecision(combinedCombiners)
+  }
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - large combiners (50pts)") {
+    assertTaskPrecision(largeCombiners)
+  }
+
+  def assertTaskPrecision(combiners: List[(DLLCombinerTest, Array[Int])]): Unit = {
+    combiners.foreach { elem =>
+      var data = Array.fill(elem._1.cnt)(0)
+      var ref = Array.fill(elem._1.cnt)(0)
+      val task1 = elem._1.task1(data)
+      task1.join
+      Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 0) ref(i) = elem._2(i)))
+      assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i)))
+
+      data = Array.fill(elem._1.cnt)(0)
+      ref = Array.fill(elem._1.cnt)(0)
+      val task2 = elem._1.task2(data)
+      task2.join
+      Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 1) ref(i) = elem._2(i)))
+      assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i)))
+
+      data = Array.fill(elem._1.cnt)(0)
+      ref = Array.fill(elem._1.cnt)(0)
+      val task3 = elem._1.task3(data)
+      task3.join
+      Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 == elem._1.cnt % 2) ref(i) = elem._2(i)))
+      assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i)))
+
+      data = Array.fill(elem._1.cnt)(0)
+      ref = Array.fill(elem._1.cnt)(0)
+      val task4 = elem._1.task4(data)
+      Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 != elem._1.cnt % 2) ref(i) = elem._2(i)))
+      assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i)))
+    }
+  }
+
+  test("[Public] fetch simple result without combining (5pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    combiner1 += 7
+    combiner1 += 2
+    combiner1 += 3
+    combiner1 += 8
+    combiner1 += 1
+    combiner1 += 2
+    combiner1 += 3
+    combiner1 += 8
+
+    val result = combiner1.result()
+    val array = Array(7, 2, 3, 8, 1, 2, 3, 8)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+  test("[Public] fetch result without combining (5pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    combiner1 += 7
+    combiner1 += 2
+    combiner1 += 3
+    combiner1 += 8
+    combiner1 += 1
+
+    val result = combiner1.result()
+    val array = Array(7, 2, 3, 8, 1)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+  test("[Public] fetch result after simple combining (5pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    combiner1 += 7
+    combiner1 += 2
+
+    val combiner2 = DLLCombinerTest(2)
+    combiner2 += 3
+    combiner2 += 8
+
+    val combiner3 = DLLCombinerTest(2)
+    combiner2 += 1
+    combiner2 += 9
+
+    val combiner4 = DLLCombinerTest(2)
+    combiner2 += 3
+    combiner2 += 2
+
+    val result = combiner1.combine(combiner2).combine(combiner3).combine(combiner4).result()
+    val array = Array(7, 2, 3, 8, 1, 9, 3, 2)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+  test("[Public] fetch result - empty combiner (20pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    val result = combiner1.result()
+    assertEquals(result.size, 0)
+  }
+
+  test("[Public] fetch result - full single element combiner (15pts)") {
+    val combiner1 = DLLCombinerTest(3)
+    combiner1 += 4
+    combiner1 += 2
+    combiner1 += 6
+
+    val result = combiner1.result()
+    val array = Array(4, 2, 6)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+}
+
+
+trait AbstractM6Suite extends munit.FunSuite with LibImpl {
+
+  def simpleCombiners = buildSimpleCombiners()
+  def basicCombiners = buildBasicCombiners()
+  def combinedCombiners = buildCombinedCombiners()
+  def largeCombiners = buildLargeCombiners()
+
+  def buildSimpleCombiners() = {
+    val simpleCombiners = List(
+      (DLLCombinerTest(4), Array(4, 2, 6, 1, 5, 4, 3, 5, 6, 3, 4, 5, 6, 3, 4, 5)),
+      (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2)),
+      (DLLCombinerTest(4), Array.fill(16)(5))
+    )
+    simpleCombiners.foreach(elem => build(elem._1, elem._2))
+    simpleCombiners
+  }
+
+  def buildBasicCombiners() = {
+    val basicCombiners = List(
+      (DLLCombinerTest(2), Array(4, 2, 6)),
+      (DLLCombinerTest(5), Array(4, 2, 6)),
+      (DLLCombinerTest(3), Array(4, 2, 6, 1, 7, 2, 4)),
+      (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 11, 12, 13, 14, 15, 16, 17, 22)),
+      (DLLCombinerTest(3), Array.fill(16)(7)),
+      (DLLCombinerTest(3), Array.fill(19)(7)),
+      (DLLCombinerTest(3), Array.fill(5)(7)),
+      (DLLCombinerTest(3), Array.fill(6)(7))
+    )
+    basicCombiners.foreach(elem => build(elem._1, elem._2))
+    basicCombiners
+  }
+
+  def buildCombinedCombiners() = {
+    var combinedCombiners = List[(DLLCombinerTest, Array[Int])]()
+    Range(1, 10).foreach { chunk_size =>
+      val array = basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foldLeft(Array[Int]()) { (acc, i) => acc ++ i._2 }
+      val combiner = DLLCombinerTest(chunk_size)
+      basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foreach(elem => combiner.combine(elem._1))
+
+      combinedCombiners  = combinedCombiners :+ (combiner, array)
+    }
+    combinedCombiners
+  }
+
+  def buildLargeCombiners() = {
+    val largeCombiners = List(
+      (DLLCombinerTest(21), Array.fill(1321)(4) ++ Array.fill(1322)(7)),
+      (DLLCombinerTest(18), Array.fill(1341)(2) ++ Array.fill(1122)(5)),
+      (DLLCombinerTest(3), Array.fill(1321)(4) ++ Array.fill(1322)(7) ++ Array.fill(321)(4) ++ Array.fill(322)(7)),
+      (DLLCombinerTest(12), Array.fill(992321)(4) ++ Array.fill(99322)(7)),
+      (DLLCombinerTest(4), Array.fill(953211)(4) ++ Array.fill(999322)(1))
+    )
+    largeCombiners.foreach(elem => build(elem._1, elem._2))
+    largeCombiners
+  }
+
+  def build(combiner: DLLCombinerTest, array: Array[Int]): DLLCombinerTest = {
+    array.foreach(elem => combiner += elem)
+    combiner
+  }
+
+  def compare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = {
+    val result = combiner.result()
+    Range(0,array.size).forall(i => array(i) == result(i))
+  }
+
+  def buildAndCompare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = {
+    array.foreach(elem => combiner += elem)
+    val result = combiner.result()
+
+    Range(0,array.size).forall(i => array(i) == result(i))
+  }
+
+}
+
+trait LibImpl extends M6 {
+
+  val forkJoinPool = new ForkJoinPool
+
+  abstract class TaskScheduler {
+    def schedule[T](body: => T): ForkJoinTask[T]
+  }
+
+  class DefaultTaskScheduler extends TaskScheduler {
+    def schedule[T](body: => T): ForkJoinTask[T] = {
+      val t = new RecursiveTask[T] {
+        def compute = body
+      }
+      Thread.currentThread match {
+        case wt: ForkJoinWorkerThread =>
+          t.fork()
+        case _ =>
+          forkJoinPool.execute(t)
+      }
+      t
+    }
+  }
+
+  val scheduler =
+    new DynamicVariable[TaskScheduler](new DefaultTaskScheduler)
+
+  def task[T](body: => T): ForkJoinTask[T] = {
+    scheduler.value.schedule(body)
+  }
+
+  class DLLCombinerTest(chunk_size: Int = 3) extends DLLCombinerImplementation(chunk_size) {
+
+    override def +=(elem: Int): Unit = {
+      if(cnt % chunk_size == 0) {
+        chunks = chunks + 1
+        val node = new Node(chunk_size)
+        if (cnt == 0) {
+          head = node
+          last = node
+        }
+        else {
+          last.next = node
+          node.previous = last
+          last = node
+        }
+      }
+      last.add(elem)
+      cnt += 1
+    }
+
+    override def combine(that: DLLCombiner): DLLCombiner = {
+      assert(this.chunk_size == that.chunk_size)
+      if (this.cnt == 0) {
+        this.head = that.head
+        this.last = that.last
+        this.cnt = that.cnt
+        this.chunks = that.chunks
+
+        this
+      }
+      else if (that.cnt == 0)
+        this
+      else {
+        this.last.next = that.head
+        that.head.previous = this.last
+
+        this.cnt = this.cnt + that.cnt
+        this.chunks = this.chunks + that.chunks
+        this.last = that.last
+
+        this
+      }
+    }
+  }
+}
diff --git a/previous-exams/2021-midterm-solutions/m7.md b/previous-exams/2021-midterm/m7.md
similarity index 97%
rename from previous-exams/2021-midterm-solutions/m7.md
rename to previous-exams/2021-midterm/m7.md
index 899703e0c7cfc8212fc6c2f95688d204bbf1a014..45a74f060eb38b9d95195c1eeaedd1bfa9dd06ed 100644
--- a/previous-exams/2021-midterm-solutions/m7.md
+++ b/previous-exams/2021-midterm/m7.md
@@ -1,9 +1,3 @@
-Use the following commands to make a fresh clone of your repository:
-
-```
-git clone -b m7 git@gitlab.epfl.ch:lamp/student-repositories-s21/cs206-GASPAR.git m7
-```
-
 ## Useful links
 
   * [A guide to the Scala parallel collections](https://docs.scala-lang.org/overviews/parallel-collections/overview.html)
diff --git a/previous-exams/2021-midterm/m7/.gitignore b/previous-exams/2021-midterm/m7/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40937dc9b192820d0ede18efd3c7e6442a083b17
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/.gitignore
@@ -0,0 +1,22 @@
+# General
+*.DS_Store
+*.swp
+*~
+
+# Dotty
+*.class
+*.tasty
+*.hasTasty
+
+# sbt
+target/
+
+# IDE
+.bsp
+.bloop
+.metals
+.vscode
+
+# datasets
+stackoverflow-grading.csv
+wikipedia-grading.dat
diff --git a/previous-exams/2021-midterm/m7/assignment.sbt b/previous-exams/2021-midterm/m7/assignment.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..da7eb3c8347293a18da0025fcd6060d8f8f7cc11
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/assignment.sbt
@@ -0,0 +1,2 @@
+// Student tasks (i.e. submit, packageSubmission)
+enablePlugins(StudentTasks)
diff --git a/previous-exams/2021-midterm/m7/build.sbt b/previous-exams/2021-midterm/m7/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..25a0926a0020518f0905ffb468b8daccfb2489c4
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/build.sbt
@@ -0,0 +1,12 @@
+course := "midterm"
+assignment := "m7"
+scalaVersion := "3.0.0-RC1"
+scalacOptions ++= Seq("-language:implicitConversions", "-deprecation")
+
+libraryDependencies += "org.scalameta" %% "munit" % "0.7.22"
+
+val MUnitFramework = new TestFramework("munit.Framework")
+testFrameworks += MUnitFramework
+// Decode Scala names
+testOptions += Tests.Argument(MUnitFramework, "-s")
+testSuite := "m7.M7Suite"
diff --git a/previous-exams/2021-midterm/m7/grading-tests.jar b/previous-exams/2021-midterm/m7/grading-tests.jar
new file mode 100644
index 0000000000000000000000000000000000000000..4260440385397b26a3f9b531aac5592925d392b3
Binary files /dev/null and b/previous-exams/2021-midterm/m7/grading-tests.jar differ
diff --git a/previous-exams/2021-midterm/m7/project/FilteringReporterPlugin.scala b/previous-exams/2021-midterm/m7/project/FilteringReporterPlugin.scala
new file mode 100644
index 0000000000000000000000000000000000000000..2e4fd9a4d998698cd52643344b33a5e719dd7971
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/project/FilteringReporterPlugin.scala
@@ -0,0 +1,31 @@
+package sbt // To access the private[sbt] compilerReporter key
+package filteringReporterPlugin
+
+import Keys._
+import ch.epfl.lamp._
+
+object FilteringReporterPlugin extends AutoPlugin {
+  override lazy val projectSettings = Seq(
+    // Turn off warning coming from scalameter that we cannot fix without changing scalameter
+    compilerReporter in (Compile, compile) ~= { reporter => new FilteringReporter(reporter) }
+  )
+}
+
+class FilteringReporter(reporter: xsbti.Reporter) extends xsbti.Reporter {
+
+  def reset(): Unit = reporter.reset()
+  def hasErrors: Boolean = reporter.hasErrors
+  def hasWarnings: Boolean = reporter.hasWarnings
+  def printSummary(): Unit = reporter.printSummary()
+  def problems: Array[xsbti.Problem] = reporter.problems
+
+  def log(problem: xsbti.Problem): Unit = {
+    if (!problem.message.contains("An existential type that came from a Scala-2 classfile cannot be"))
+      reporter.log(problem)
+  }
+
+  def comment(pos: xsbti.Position, msg: String): Unit =
+    reporter.comment(pos, msg)
+
+  override def toString = s"CollectingReporter($reporter)"
+}
diff --git a/previous-exams/2021-midterm/m7/project/MOOCSettings.scala b/previous-exams/2021-midterm/m7/project/MOOCSettings.scala
new file mode 100644
index 0000000000000000000000000000000000000000..1c40443a53085d23fadb134f4e1a505c32231f1d
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/project/MOOCSettings.scala
@@ -0,0 +1,49 @@
+package ch.epfl.lamp
+
+import sbt._
+import sbt.Keys._
+
+/**
+ * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have
+ * different item ids.
+ *
+ * @param key Assignment key
+ * @param partId Assignment partId
+ * @param itemId Item id of the non premium version
+ * @param premiumItemId Item id of the premium version (`None` if the assignment is optional)
+ */
+case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String])
+
+/**
+  * Settings shared by all assignments, reused in various tasks.
+  */
+object MOOCSettings extends AutoPlugin {
+
+  override def requires = super.requires && filteringReporterPlugin.FilteringReporterPlugin
+
+  object autoImport {
+    val course = SettingKey[String]("course")
+    val assignment = SettingKey[String]("assignment")
+    val options = SettingKey[Map[String, Map[String, String]]]("options")
+    val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment")
+    val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment")
+      .withRank(KeyRanks.Invisible)
+    // Convenient alias
+    type CourseraId = ch.epfl.lamp.CourseraId
+    val CourseraId = ch.epfl.lamp.CourseraId
+  }
+
+  import autoImport._
+
+  override val globalSettings: Seq[Def.Setting[_]] = Seq(
+    // supershell is verbose, buggy and useless.
+    useSuperShell := false
+  )
+
+  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,
+    name := s"${course.value}-${assignment.value}"
+  )
+}
diff --git a/previous-exams/2021-midterm/m7/project/StudentTasks.scala b/previous-exams/2021-midterm/m7/project/StudentTasks.scala
new file mode 100644
index 0000000000000000000000000000000000000000..c4669afe82dd2b45651f94dcad9e736f29d21432
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/project/StudentTasks.scala
@@ -0,0 +1,303 @@
+package ch.epfl.lamp
+
+import sbt._
+import Keys._
+
+// import scalaj.http._
+import java.io.{File, FileInputStream, IOException}
+import org.apache.commons.codec.binary.Base64
+// import play.api.libs.json.{Json, JsObject, JsPath}
+import scala.util.{Failure, Success, Try}
+
+/**
+  * Provides tasks for submitting the assignment
+  */
+object StudentTasks extends AutoPlugin {
+
+  override def requires = super.requires && MOOCSettings
+
+  object autoImport {
+    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")
+
+    lazy val Grading = config("grading") extend(Runtime)
+  }
+
+
+  import autoImport._
+  import MOOCSettings.autoImport._
+
+  override lazy val projectSettings = Seq(
+    packageSubmissionSetting,
+    fork := true,
+    connectInput in run := true,
+    outputStrategy := Some(StdoutOutput),
+  ) ++
+    packageSubmissionZipSettings ++
+    inConfig(Grading)(Defaults.testSettings ++ Seq(
+      unmanagedJars += file("grading-tests.jar"),
+
+      definedTests := (definedTests in Test).value,
+      internalDependencyClasspath := (internalDependencyClasspath in Test).value
+    ))
+
+
+  /** **********************************************************
+    * 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, None)
+      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 := {
+    // Fail if scalafix linting does not pass.
+    scalafixLinting.value
+
+    val args: Seq[String] = Def.spaceDelimited("<arg>").parsed
+    val s: TaskStreams = streams.value // for logging
+    val jar = (packageSubmissionZip in Compile).value
+
+    val assignmentDetails =
+      courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined"))
+    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 <email-address> <submit-token>
+              |
+              |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/previous-exams/2021-midterm/m7/project/build.properties b/previous-exams/2021-midterm/m7/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0b2e09c5ac99bd3de91b2b139b94301c2b6e26f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.4.7
diff --git a/previous-exams/2021-midterm/m7/project/buildSettings.sbt b/previous-exams/2021-midterm/m7/project/buildSettings.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..8fac702aaf3f3c4ede79691c7b4e4a52f26f3f47
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/project/buildSettings.sbt
@@ -0,0 +1,5 @@
+// Used for Coursera submission (StudentPlugin)
+// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2"
+// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
+// Used for Base64 (StudentPlugin)
+libraryDependencies += "commons-codec" % "commons-codec" % "1.10"
diff --git a/previous-exams/2021-midterm/m7/project/plugins.sbt b/previous-exams/2021-midterm/m7/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..fb7dbe068109e7f35c13b2762b865c7eec1979f3
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/project/plugins.sbt
@@ -0,0 +1,3 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
+addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3")
diff --git a/previous-exams/2021-midterm/m7/src/main/scala/m7/M7.scala b/previous-exams/2021-midterm/m7/src/main/scala/m7/M7.scala
new file mode 100644
index 0000000000000000000000000000000000000000..20afd03b796e95251fa3c92993066c7bebfe6ea8
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/src/main/scala/m7/M7.scala
@@ -0,0 +1,69 @@
+package m7
+
+import java.util.concurrent._
+import scala.util.DynamicVariable
+
+trait M7 extends Lib {
+
+  class DLLCombinerImplementation(chunk_size: Int = 3) extends DLLCombiner(chunk_size){
+
+    // Computes every other Integer element of data array, starting from the second, up to the middle
+    def task1(data: Array[Int]): ForkJoinTask[Unit] = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the first (index 0), up to the middle
+    def task2(data: Array[Int]): ForkJoinTask[Unit] = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the last, up to the middle
+    def task3(data: Array[Int]): ForkJoinTask[Unit] = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the second to last, up to the middle
+    // This is executed on the current thread.
+    def task4(data: Array[Int]): Unit = {
+      ???
+    }
+
+    def result(): Array[Int] = {
+      val data = new Array[Int](cnt)
+
+      ???
+
+      data
+    }
+
+    private def copyForward(data: Array[Int], curr: Node, from: Int, to: Int, limit: Int) = {
+      var current = curr
+      var i_from = from
+      var i_to = to 
+
+      while (i_to < limit) {
+        try {
+          data(i_to) = current.value(i_from)
+          i_to += 2
+          i_from += 2
+          if(i_from == current.cnt){
+            current = current.next
+            i_from = 0
+          }
+          else if(i_from > current.cnt) {
+            current = current.next
+            i_from = 1
+            if(current.cnt == 1) {
+              current = current.next
+              i_from = 0
+            }
+          }
+        }
+        catch{
+          case e: Exception =>
+        }
+      }
+    }
+  }
+
+}
diff --git a/previous-exams/2021-midterm/m7/src/main/scala/m7/lib.scala b/previous-exams/2021-midterm/m7/src/main/scala/m7/lib.scala
new file mode 100644
index 0000000000000000000000000000000000000000..20eda60c5fe1bae1271615650af917ff08929690
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/src/main/scala/m7/lib.scala
@@ -0,0 +1,84 @@
+package m7
+
+import java.util.concurrent._
+import scala.util.DynamicVariable
+
+trait Lib {
+
+  class Node(val size: Int) {
+    var value: Array[Int] = new Array(size)
+    var next: Node = null    
+    var previous: Node = null
+    var cnt = 0
+
+    def add(v: Int) = {
+      value(cnt) = v
+      cnt += 1
+    }
+  }
+
+  // Simplified Combiner interface
+  // Implements methods += and combine
+  // Abstract methods should be implemented in subclasses
+  abstract class DLLCombiner(val chunk_size: Int) {
+    var head: Node = null
+    var last: Node = null
+    var cnt: Int = 0
+    var chunks: Int = 0
+
+    // Adds an Integer to the last node of this array combiner. If the last node is full, allocates a new node.
+    def +=(elem: Int): Unit = {
+      if(cnt % chunk_size == 0) {
+        chunks = chunks + 1
+        val node = new Node(chunk_size)
+        if (cnt == 0) {
+          head = node
+          last = node
+        }
+        else {
+          last.next = node
+          node.previous = last
+          last = node
+        }
+      }
+      last.add(elem)
+      cnt += 1
+    }
+
+    // Combines this array combiner and another given combiner in constant O(1) complexity.
+    def combine(that: DLLCombiner): DLLCombiner = {
+      assert(this.chunk_size == that.chunk_size)
+      if (this.cnt == 0) {
+        this.head = that.head
+        this.last = that.last
+        this.cnt = that.cnt
+        this.chunks = that.chunks
+
+        this
+      }
+      else if (that.cnt == 0)
+        this
+      else {
+        this.last.next = that.head
+        that.head.previous = this.last
+
+        this.cnt = this.cnt + that.cnt
+        this.chunks = this.chunks + that.chunks
+        this.last = that.last
+
+        this
+      }
+    }
+
+    def task1(data: Array[Int]): ForkJoinTask[Unit]
+    def task2(data: Array[Int]): ForkJoinTask[Unit]
+    def task3(data: Array[Int]): ForkJoinTask[Unit]
+    def task4(data: Array[Int]): Unit
+
+    def result(): Array[Int]
+
+  }
+
+  def task[T](body: => T): ForkJoinTask[T]
+
+}
\ No newline at end of file
diff --git a/previous-exams/2021-midterm/m7/src/test/scala/m7/M7Suite.scala b/previous-exams/2021-midterm/m7/src/test/scala/m7/M7Suite.scala
new file mode 100644
index 0000000000000000000000000000000000000000..8e1c344eacca6b10c5646a7318fb1c0c19252bc5
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/src/test/scala/m7/M7Suite.scala
@@ -0,0 +1,422 @@
+package m7
+
+import java.util.concurrent._
+import scala.util.DynamicVariable
+
+class M7Suite extends AbstractM7Suite {
+
+  // (70+) 30 / 500 points for correct implementation, don't check parallelism
+  test("[Correctness] fetch result - simple combiners (5pts)") {
+    assertCorrectnessSimple()
+  }
+
+  test("[Correctness] fetch result - small combiners (5pts)") {
+    assertCorrectnessBasic()
+  }
+
+  test("[Correctness] fetch result - small combiners after combining (10pts)") {
+    assertCorrectnessCombined()
+  }
+
+  test("[Correctness] fetch result - large combiners (10pts)") {
+    assertCorrectnessLarge()
+  }
+
+  def assertCorrectnessSimple() = {
+    simpleCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  def assertCorrectnessBasic() = {
+    basicCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  def assertCorrectnessCombined() = {
+    combinedCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  def assertCorrectnessLarge() = {
+    largeCombiners.foreach(elem => assert(compare(elem._1, elem._2)))
+  }
+
+  // (70+30+) 50 / 500 points for correct parallel implementation, don't check if it's exactly 1/4 of the array per task
+  private var count = 0
+  private val expected = 3
+
+  override def task[T](body: => T): ForkJoinTask[T] = {
+    count += 1
+    scheduler.value.schedule(body)
+  }
+
+  test("[TaskCount] number of newly created tasks should be 3 (5pts)") {
+    assertTaskCountSimple()
+  }
+
+  test("[TaskCount] fetch result and check parallel - simple combiners (5pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessSimple()
+  }
+
+  test("[TaskCount] fetch result and check parallel - small combiners (10pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessBasic()
+  }
+
+  test("[TaskCount] fetch result and check parallel - small combiners after combining (15pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessCombined()
+  }
+
+  test("[TaskCount] fetch result and check parallel - large combiners (15pts)") {
+    assertTaskCountSimple()
+    assertCorrectnessLarge()
+  }
+
+  def assertTaskCountSimple(): Unit = {
+    simpleCombiners.foreach(elem => assertTaskCount(elem._1, elem._2))
+  }
+
+  def assertTaskCount(combiner: DLLCombinerTest, array: Array[Int]): Unit = {
+    try {
+      count = 0
+      build(combiner, array)
+      combiner.result()
+      assertEquals(count, expected, {
+        s"ERROR: Expected $expected instead of $count calls to `task(...)`"
+      })
+    } finally {
+      count = 0
+    }
+  }
+
+  //(70+30+50+) 200 / 500 points for correct parallel implementation, exactly 1/4 of the array per task
+  test("[TaskFairness] each task should compute 1/4 of the result (50pts)") {
+    assertTaskFairness(simpleCombiners.unzip._1)
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - simple combiners (20pts)") {
+    assertTaskFairness(simpleCombiners.unzip._1)
+    assertCorrectnessSimple()
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners (30pts)") {
+    assertTaskFairness(basicCombiners.unzip._1)
+    assertCorrectnessBasic()
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - small combiners after combining (50pts)") {
+    assertTaskFairness(combinedCombiners.unzip._1)
+    assertCorrectnessCombined()
+  }
+
+  test("[TaskFairness] each task should correctly compute 1/4 of the result - large combiners (50pts)") {
+    assertTaskFairness(largeCombiners.unzip._1)
+    assertCorrectnessLarge()
+  }
+
+  def assertTaskFairness(combiners: List[DLLCombinerTest]): Unit = {
+    def assertNewTaskFairness(combiner: DLLCombinerTest, task: ForkJoinTask[Unit], data: Array[Int]) = {
+      var count = 0
+      var expected = combiner.cnt / 4
+      task.join
+      count = data.count(elem => elem != 0)
+
+      assert((count - expected).abs <= 1)
+    }
+
+    def assertMainTaskFairness(combiner: DLLCombinerTest, task: Unit, data: Array[Int]) = {
+      var count = 0
+      var expected = combiner.cnt / 4
+      count = data.count(elem => elem != 0)
+
+      assert((count - expected).abs <= 1)
+    }
+
+    combiners.foreach { elem =>
+      var data = Array.fill(elem.cnt)(0)
+      assertNewTaskFairness(elem, elem.task1(data), data)
+
+      data = Array.fill(elem.cnt)(0)
+      assertNewTaskFairness(elem, elem.task2(data), data)
+
+      data = Array.fill(elem.cnt)(0)
+      assertNewTaskFairness(elem, elem.task3(data), data)
+
+      data = Array.fill(elem.cnt)(0)
+      assertMainTaskFairness(elem, elem.task4(data), data)
+    }
+  }
+
+  //(70+30+50+200+) 150 / 500 points for correct parallel implementation, exactly 1/4 of the array per task, exactly the specified quarter
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - simple combiners (20pts)") {
+    assertTaskPrecision(simpleCombiners)
+  }
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners (30pts)") {
+    assertTaskPrecision(basicCombiners)
+  }
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - small combiners after combining (50pts)") {
+    assertTaskPrecision(combinedCombiners)
+  }
+
+  test("[TaskPrecision] each task should compute specified 1/4 of the result - large combiners (50pts)") {
+    assertTaskPrecision(largeCombiners)
+  }
+
+  def assertTaskPrecision(combiners: List[(DLLCombinerTest, Array[Int])]): Unit = {
+    combiners.foreach { elem =>
+      var data = Array.fill(elem._1.cnt)(0)
+      var ref = Array.fill(elem._1.cnt)(0)
+      val task1 = elem._1.task1(data)
+      task1.join
+      Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 1) ref(i) = elem._2(i)))
+      assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i)))
+
+      data = Array.fill(elem._1.cnt)(0)
+      ref = Array.fill(elem._1.cnt)(0)
+      val task2 = elem._1.task2(data)
+      task2.join
+      Range(0, elem._1.cnt).foreach(i => (if (i < elem._1.cnt / 2 - 1 && i % 2 == 0) ref(i) = elem._2(i)))
+      assert(Range(0, elem._1.cnt / 2 - 1).forall(i => data(i) == ref(i)))
+
+      data = Array.fill(elem._1.cnt)(0)
+      ref = Array.fill(elem._1.cnt)(0)
+      val task3 = elem._1.task3(data)
+      task3.join
+      Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 != elem._1.cnt % 2) ref(i) = elem._2(i)))
+      assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i)))
+
+      data = Array.fill(elem._1.cnt)(0)
+      ref = Array.fill(elem._1.cnt)(0)
+      val task4 = elem._1.task4(data)
+      Range(0, elem._1.cnt).foreach(i => (if (i > elem._1.cnt / 2 + 1 && i % 2 == elem._1.cnt % 2) ref(i) = elem._2(i)))
+      assert(Range(elem._1.cnt / 2 + 2, elem._1.cnt).forall(i => data(i) == ref(i)))
+    }
+  }
+
+  test("[Public] fetch simple result without combining (5pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    combiner1 += 7
+    combiner1 += 2
+    combiner1 += 3
+    combiner1 += 8
+    combiner1 += 1
+    combiner1 += 2
+    combiner1 += 3
+    combiner1 += 8
+
+    val result = combiner1.result()
+    val array = Array(7, 2, 3, 8, 1, 2, 3, 8)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+  test("[Public] fetch result without combining (5pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    combiner1 += 7
+    combiner1 += 2
+    combiner1 += 3
+    combiner1 += 8
+    combiner1 += 1
+
+    val result = combiner1.result()
+    val array = Array(7, 2, 3, 8, 1)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+  test("[Public] fetch result after simple combining (5pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    combiner1 += 7
+    combiner1 += 2
+
+    val combiner2 = DLLCombinerTest(2)
+    combiner2 += 3
+    combiner2 += 8
+
+    val combiner3 = DLLCombinerTest(2)
+    combiner2 += 1
+    combiner2 += 9
+
+    val combiner4 = DLLCombinerTest(2)
+    combiner2 += 3
+    combiner2 += 2
+
+    val result = combiner1.combine(combiner2).combine(combiner3).combine(combiner4).result()
+    val array = Array(7, 2, 3, 8, 1, 9, 3, 2)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+  test("[Public] fetch result - empty combiner (20pts)") {
+    val combiner1 = DLLCombinerTest(2)
+    val result = combiner1.result()
+    assertEquals(result.size, 0)
+  }
+
+  test("[Public] fetch result - full single element combiner (15pts)") {
+    val combiner1 = DLLCombinerTest(3)
+    combiner1 += 4
+    combiner1 += 2
+    combiner1 += 6
+
+    val result = combiner1.result()
+    val array = Array(4, 2, 6)
+
+    assert(Range(0,array.size).forall(i => array(i) == result(i)))
+  }
+
+}
+
+
+trait AbstractM7Suite extends munit.FunSuite with LibImpl {
+
+  def simpleCombiners = buildSimpleCombiners()
+  def basicCombiners = buildBasicCombiners()
+  def combinedCombiners = buildCombinedCombiners()
+  def largeCombiners = buildLargeCombiners()
+
+  def buildSimpleCombiners() = {
+    val simpleCombiners = List(
+      (DLLCombinerTest(4), Array(4, 2, 6, 1, 5, 4, 3, 5, 6, 3, 4, 5, 6, 3, 4, 5)),
+      (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2)),
+      (DLLCombinerTest(4), Array.fill(16)(5))
+    )
+    simpleCombiners.foreach(elem => build(elem._1, elem._2))
+    simpleCombiners
+  }
+
+  def buildBasicCombiners() = {
+    val basicCombiners = List(
+      (DLLCombinerTest(2), Array(4, 2, 6)),
+      (DLLCombinerTest(5), Array(4, 2, 6)),
+      (DLLCombinerTest(3), Array(4, 2, 6, 1, 7, 2, 4)),
+      (DLLCombinerTest(4), Array(7, 2, 2, 9, 3, 2, 11, 12, 13, 14, 15, 16, 17, 22)),
+      (DLLCombinerTest(3), Array.fill(16)(7)),
+      (DLLCombinerTest(3), Array.fill(19)(7)),
+      (DLLCombinerTest(3), Array.fill(5)(7)),
+      (DLLCombinerTest(3), Array.fill(6)(7))
+    )
+    basicCombiners.foreach(elem => build(elem._1, elem._2))
+    basicCombiners
+  }
+
+  def buildCombinedCombiners() = {
+    var combinedCombiners = List[(DLLCombinerTest, Array[Int])]()
+    Range(1, 10).foreach { chunk_size =>
+      val array = basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foldLeft(Array[Int]()) { (acc, i) => acc ++ i._2 }
+      val combiner = DLLCombinerTest(chunk_size)
+      basicCombiners.filter(elem => elem._1.chunk_size == chunk_size).foreach(elem => combiner.combine(elem._1))
+
+      combinedCombiners  = combinedCombiners :+ (combiner, array)
+    }
+    combinedCombiners
+  }
+
+  def buildLargeCombiners() = {
+    val largeCombiners = List(
+      (DLLCombinerTest(21), Array.fill(1321)(4) ++ Array.fill(1322)(7)),
+      (DLLCombinerTest(18), Array.fill(1341)(2) ++ Array.fill(1122)(5)),
+      (DLLCombinerTest(3), Array.fill(1321)(4) ++ Array.fill(1322)(7) ++ Array.fill(321)(4) ++ Array.fill(322)(7)),
+      (DLLCombinerTest(12), Array.fill(992321)(4) ++ Array.fill(99322)(7)),
+      (DLLCombinerTest(4), Array.fill(953211)(4) ++ Array.fill(999322)(1))
+    )
+    largeCombiners.foreach(elem => build(elem._1, elem._2))
+    largeCombiners
+  }
+
+  def build(combiner: DLLCombinerTest, array: Array[Int]): DLLCombinerTest = {
+    array.foreach(elem => combiner += elem)
+    combiner
+  }
+
+  def compare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = {
+    val result = combiner.result()
+    Range(0,array.size).forall(i => array(i) == result(i))
+  }
+
+  def buildAndCompare(combiner: DLLCombinerTest, array: Array[Int]): Boolean = {
+    array.foreach(elem => combiner += elem)
+    val result = combiner.result()
+
+    Range(0,array.size).forall(i => array(i) == result(i))
+  }
+
+}
+
+trait LibImpl extends M7 {
+
+  val forkJoinPool = new ForkJoinPool
+
+  abstract class TaskScheduler {
+    def schedule[T](body: => T): ForkJoinTask[T]
+  }
+
+  class DefaultTaskScheduler extends TaskScheduler {
+    def schedule[T](body: => T): ForkJoinTask[T] = {
+      val t = new RecursiveTask[T] {
+        def compute = body
+      }
+      Thread.currentThread match {
+        case wt: ForkJoinWorkerThread =>
+          t.fork()
+        case _ =>
+          forkJoinPool.execute(t)
+      }
+      t
+    }
+  }
+
+  val scheduler =
+    new DynamicVariable[TaskScheduler](new DefaultTaskScheduler)
+
+  def task[T](body: => T): ForkJoinTask[T] = {
+    scheduler.value.schedule(body)
+  }
+
+  class DLLCombinerTest(chunk_size: Int = 3) extends DLLCombinerImplementation(chunk_size) {
+
+    override def +=(elem: Int): Unit = {
+      if(cnt % chunk_size == 0) {
+        chunks = chunks + 1
+        val node = new Node(chunk_size)
+        if (cnt == 0) {
+          head = node
+          last = node
+        }
+        else {
+          last.next = node
+          node.previous = last
+          last = node
+        }
+      }
+      last.add(elem)
+      cnt += 1
+    }
+
+    override def combine(that: DLLCombiner): DLLCombiner = {
+      assert(this.chunk_size == that.chunk_size)
+      if (this.cnt == 0) {
+        this.head = that.head
+        this.last = that.last
+        this.cnt = that.cnt
+        this.chunks = that.chunks
+
+        this
+      }
+      else if (that.cnt == 0)
+        this
+      else {
+        this.last.next = that.head
+        that.head.previous = this.last
+
+        this.cnt = this.cnt + that.cnt
+        this.chunks = this.chunks + that.chunks
+        this.last = that.last
+
+        this
+      }
+    }
+  }
+}