diff --git a/previous-exams/2021-midterm/m1.md b/previous-exams/2021-midterm/m1.md
new file mode 100644
index 0000000000000000000000000000000000000000..0846974688e1aee42c5b8c7b7a7460f4aeeeb176
--- /dev/null
+++ b/previous-exams/2021-midterm/m1.md
@@ -0,0 +1,61 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+Given the following sequential implementation of a function that computes the sequence of rolling averages, your task will be to complete and optimize a parallel version of this code.
+
+```scala
+/** 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 rollingAveragesSequential(arr: Array[Int]): Array[Double] =
+  // Transform all numbers to fractions with denominator 1
+  val arr1 = arr.map(x => Frac(x, 1))
+  // Compute the rolling average keeping the sum of all elements in the numerator and the count of elements in the denominator.
+  val arr2 = arr1.scan(Frac(0, 0))((acc, x) => Frac(acc.numerator + x.numerator, acc.denominator + x.denominator))
+  // Transform fractions to Doubles
+  arr2.map(frac => frac.toDouble)
+  // Drop the extra initial element that was added by the scan
+  arr3.tail
+```
+
+ This implementation has some issues:
+ - It does not use parallelism
+ - Creates two intermediate arrays by calling `map`
+ - Creates an extra intermediate arrays by calling `tail`
+ - Scan returns an extra element we do not need
+
+ We want to parallelize and avoid the creation of the extra arrays.
+ As we are calling a `scan` the natural operations we need are `upsweep` and `downsweep`.
+ It is possible specialize those operations for our problem by letting those operations do the mapping.
+ It is also possible to change those operations to not generate the first element.
+
+We give you a version of `rollingAveragesSequential` that partially implements the parallelization using `upsweep` and `downsweep`.
+
+ Your tasks in the exercise will be to:
+ - TASK 1: Implement the parallelization of `upsweep` and `downsweep`
+ - TASK 2: Remove the calls to the `map`
+ - TASK 3: Remove the call to `tail`
+
+ You can get partial points for solving part of the tasks.
+ The order of the tasks is a suggestion, you may do them in any order if that is simpler for you.
+
+Look at the `Lib` trait to find the definitions of functions and classes you can use (or already used).
+In this question we use a `Arr` array class instead of the normal `Array`. You may assume that this class has the same performance characteristics as the normal array. `Arr` provides only a limited set of operations.
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..6fa91f4263cecf0bd58cfea49fefc29d3cf7bffa
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..fc5399100b3641d1d7fd3189258f4fb5e706fe7d
--- /dev/null
+++ b/previous-exams/2021-midterm/m1/src/main/scala/m1/M1.scala
@@ -0,0 +1,76 @@
+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)
+
+    val out: Arr[Double] = Arr.ofLength(arr.length)
+    val tree = upsweep(arr, 0, arr.length)
+    downsweep(arr, Frac(0, 0), tree, out)
+    out
+  }
+
+  // No need to modify this
+  def scanOp(acc: Frac, x: Frac) =
+    Frac(acc.numerator + x.numerator, acc.denominator + x.denominator)
+
+
+  def upsweep(input: Arr[Int], from: Int, to: Int): TreeRes = {
+    if (to - from < THRESHOLD)
+      Leaf(from, to, reduceSequential(input, from + 1, to, Frac(input(from), 1)))
+    else {
+      val mid = from + (to - from)/2
+      val (tL, tR) = parallel(
+        upsweep(input, from, mid),
+        upsweep(input, mid, to)
+      )
+      Node(tL, scanOp(tL.res, tR.res), tR)
+    }
+  }
+
+
+  def downsweep(input: Arr[Int], a0: Frac, tree: TreeRes, output: Arr[Double]): Unit = {
+    tree match {
+      case Node(left, _, right) =>
+        parallel(
+          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[Int], from: Int, to: Int, a0: Frac, output: Arr[Double]): Unit = {
+    if (from < to) {
+      var i = from
+      var a = a0
+      while (i < to) {
+        a = scanOp(a, Frac(input(i), 1))
+        output(i) = a.toDouble
+        i = i + 1
+      }
+    }
+  }
+
+
+  def reduceSequential(input: Arr[Int], from: Int, to: Int, a0: Frac): Frac = {
+    var a = a0
+    var i = from
+    while (i < to) {
+      a = scanOp(a, Frac(input(i), 1))
+      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/m14.md b/previous-exams/2021-midterm/m14.md
new file mode 100644
index 0000000000000000000000000000000000000000..62f775d8a176774aef56064544cb4935ff71e5ee
--- /dev/null
+++ b/previous-exams/2021-midterm/m14.md
@@ -0,0 +1,42 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+In this question, you will implement a simple thread pool executor.
+Thread pool executors provide a way to execute tasks in parallel using one of several pooled threads.
+Using a pool of threads provides improved performance compared to creating a new thread for every operation since threads in the pool are reused throughout the executor's lifetime.
+
+Thread pool executors are one of the core primitive used to implement parallel programs. For example, they are the underlying mechanism used in the implementation of `Future`-s.
+
+Your task is to complete the implementation of the `ThreadPoolExecutor` class. This class' constructor takes the number of threads of the pool as argument and exposes two life-cycle methods (`start` and `shutdown`), and an `execute` method to run tasks on the thread pool. The `execute` method takes as argument `Unit => Unit` functions. These functions can be constructed anonymously using the following syntax: `val func = (x: Unit) => println("hello")`. Furthermore, given a `func` function of type `Unit => Unit`, one can invoke that function using `func(())`.
+For the purpose of this exercise, you can assume that the tasks submitted to the thread pool via the `execute` method do not throw exceptions.
+
+The thread pool implementation uses two additional classes:
+- `BlockingQueue`, used by the `ThreadPoolExecutor` to store pending tasks,
+- `Worker`-s, which each run in a separate thread, consume tasks from the queue and execute those tasks.
+
+The `BlockingQueue` implements two methods:
+- `put` to insert an element in the queue
+- `take` to retrieve and remove an element from the queue, in a last in first out order.
+
+The `put` operation always succeeds and is non-blocking (the queue is unbounded).
+The `take` operation is a potentially blocking operation that waits for new elements when called on an empty queue.
+
+Given that `Worker`-s run on separate threads, the `take` operation must be thread-safe. Furthermore, since the thread pool executor could also be used from multiple threads, the `put` operation should also be thread-safe.
+Your implementation should use a single lock to achieve this thread safety, specifically using the `[wait](http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#wait())`/`[notify](http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#notify())`/`[notifyAll](http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#notifyAll())`/`synchronized` primitives.
+Remember that `wait`, `notify` and `notifyAll` should only be invoked inside a synchronized block.
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..ff7d9e97309ad2d12b3694244f6870f5603f0dbc
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..82e2e4c20f7dad9d3ad87588f31b2a566a31bef5
--- /dev/null
+++ b/previous-exams/2021-midterm/m14/src/main/scala/m14/M14.scala
@@ -0,0 +1,80 @@
+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) {
+
+            val task = taskQueue.take()
+            task(())
+          }
+        } 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 =
+
+      taskQueue.put(task)
+
+    /** 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 =
+
+      this.synchronized {
+        val underlying = getUnderlying()
+        setUnderlying(elem :: underlying)
+        this.notifyAll()
+      }
+
+
+    /**
+     * Retrieves and removes the head of this queue, waiting if necessary
+     * This queue operates in a first in, first out order.
+     * until an element becomes available (blocking).
+     * This queue operates in a last in, first out order.
+     */
+    def take(): T =
+
+      this.synchronized {
+        while (getUnderlying().isEmpty)
+          this.wait()
+        val underlying = getUnderlying()
+        val head = underlying.head
+        setUnderlying(underlying.tail)
+        head
+      }
+  }
+}
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/m15.md b/previous-exams/2021-midterm/m15.md
new file mode 100644
index 0000000000000000000000000000000000000000..781e05a4681b94b94c4f8376bec6c2fae7d592cb
--- /dev/null
+++ b/previous-exams/2021-midterm/m15.md
@@ -0,0 +1,42 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+In this question, you will implement a simple thread pool executor.
+Thread pool executors provide a way to execute tasks in parallel using one of several pooled threads.
+Using a pool of threads provides improved performance compared to creating a new thread for every operation since threads in the pool are reused throughout the executor's lifetime.
+
+Thread pool executors are one of the core primitive used to implement parallel programs. For example, they are the underlying mechanism used in the implementation of `Future`-s.
+
+Your task is to complete the implementation of the `ThreadPoolExecutor` class. This class' constructor takes the number of threads of the pool as argument and exposes two life-cycle methods (`start` and `shutdown`), and an `execute` method to run tasks on the thread pool. The `execute` method takes as argument `Unit => Unit` functions. These functions can be constructed anonymously using the following syntax: `val func = (x: Unit) => println("hello")`. Furthermore, given a `func` function of type `Unit => Unit`, one can invoke that function using `func(())`.
+For the purpose of this exercise, you can assume that the tasks submitted to the thread pool via the `execute` method do not throw exceptions.
+
+The thread pool implementation uses two additional classes:
+- `BlockingQueue`, used by the `ThreadPoolExecutor` to store pending tasks,
+- `Worker`-s, which each run in a separate thread, consume tasks from the queue and execute those tasks.
+
+The `BlockingQueue` implements two methods:
+- `put` to insert an element in the queue
+- `take` to retrieve and remove an element from the queue, in a first in first out order.
+
+The `put` operation always succeeds and is non-blocking (the queue is unbounded).
+The `take` operation is a potentially blocking operation that waits for new elements when called on an empty queue.
+
+Given that `Worker`-s run on separate threads, the `take` operation must be thread-safe. Furthermore, since the thread pool executor could also be used from multiple threads, the `put` operation should also be thread-safe.
+Your implementation should use a single lock to achieve this thread safety, specifically using the `[wait](http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#wait())`/`[notify](http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#notify())`/`[notifyAll](http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#notifyAll())`/`synchronized` primitives.
+Remember that `wait`, `notify` and `notifyAll` should only be invoked inside a synchronized block.
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..3243be21fe08c74418f052ef9b4f521129bd62f0
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..27bb83d5cdf305a47500c8fddd5b146394fdc38c
--- /dev/null
+++ b/previous-exams/2021-midterm/m15/src/main/scala/m15/M15.scala
@@ -0,0 +1,79 @@
+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) {
+
+            val task = taskQueue.take()
+            task(())
+          }
+        } 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 =
+
+      taskQueue.put(task)
+
+    /** 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 =
+
+      this.synchronized {
+        val underlying = getUnderlying()
+        setUnderlying(elem :: underlying)
+        this.notifyAll()
+      }
+
+
+    /**
+     * 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 =
+
+      this.synchronized {
+        while (getUnderlying().isEmpty)
+          this.wait()
+        val underlying = getUnderlying()
+        val last = underlying.last
+        setUnderlying(underlying.init)
+        last
+      }
+  }
+}
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/m2.md b/previous-exams/2021-midterm/m2.md
new file mode 100644
index 0000000000000000000000000000000000000000..47b097671c167b9e5f677e622ac8ce3572b09a09
--- /dev/null
+++ b/previous-exams/2021-midterm/m2.md
@@ -0,0 +1,61 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+Given the following sequential implementation of a function that computes the sequence of rolling geometric means, your task will be to complete and optimize a parallel version of this code.
+
+```scala
+/** 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] = {
+  // Transform all numbers to roots with degree 1
+  val arr1 = arr.map(x => Root(x, 1))
+  // Compute the rolling geometric mean keeping the root structured
+  val arr2 = arr1.scan(Root(1, 0))((acc, x) => Root(acc.radicand * x.radicand, acc.degree + x.degree))
+  // Transform the roots to Doubles
+  arr2.map(root => root.toDouble)
+  // Drop the extra initial element that was added by the scan
+  arr3.tail
+```
+
+ This implementation has some issues:
+ - It does not use parallelism
+ - Creates two intermediate arrays by calling `map`
+ - Creates an extra intermediate arrays by calling `tail`
+ - Scan returns an extra element we do not need
+
+ We want to parallelize and avoid the creation of the extra arrays.
+ As we are calling a `scan` the natural operations we need are `upsweep` and `downsweep`.
+ It is possible specialize those operations for our problem by letting those operations do the mapping.
+ It is also possible to change those operations to not generate the first element.
+
+We give you a version of `rollingGeoMeanSequential` that partially implements the parallelization using `upsweep` and `downsweep`.
+
+ Your tasks in the exercise will be to:
+ - TASK 1: Implement the parallelization of `upsweep` and `downsweep`
+ - TASK 2: Remove the calls to the `map`
+ - TASK 3: Remove the call to `tail`
+
+ You can get partial points for solving part of the tasks.
+ The order of the tasks is a suggestion, you may do them in any order if that is simpler for you.
+
+Look at the `Lib` trait to find the definitions of functions and classes you can use (or already used).
+In this question we use a `Arr` array class instead of the normal `Array`. You may assume that this class has the same performance characteristics as the normal array. `Arr` provides only a limited set of operations.
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..66ce3fdf704542bc3d07a5b92687caf1fd2b3cf1
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..17109982524a19500935523c27ef03a1098065f7
--- /dev/null
+++ b/previous-exams/2021-midterm/m2/src/main/scala/m2/M2.scala
@@ -0,0 +1,75 @@
+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)
+
+    val out: Arr[Double] = Arr.ofLength(arr.length)
+    val tree = upsweep(arr, 0, arr.length)
+    downsweep(arr, Root(1, 0), tree, out)
+    out
+  }
+
+  // No need to modify this
+  def scanOp(acc: Root, x: Root) =
+    Root(acc.radicand * x.radicand, acc.degree + x.degree)
+
+
+  def upsweep(input: Arr[Int], from: Int, to: Int): TreeRes = {
+    if (to - from < THRESHOLD)
+      Leaf(from, to, reduceSequential(input, from + 1, to, Root(input(from), 1)))
+    else {
+      val mid = from + (to - from)/2
+      val (tL, tR) = parallel(
+        upsweep(input, from, mid),
+        upsweep(input, mid, to)
+      )
+      Node(tL, scanOp(tL.res, tR.res), tR)
+    }
+  }
+
+
+  def downsweep(input: Arr[Int], a0: Root, tree: TreeRes, output: Arr[Double]): Unit = {
+    tree match {
+      case Node(left, _, right) =>
+        parallel(
+          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[Int], from: Int, to: Int, a0: Root, output: Arr[Double]): Unit = {
+    if (from < to) {
+      var i = from
+      var a = a0
+      while (i < to) {
+        a = scanOp(a, Root(input(i), 1))
+        output(i) = a.toDouble
+        i = i + 1
+      }
+    }
+  }
+
+
+  def reduceSequential(input: Arr[Int], from: Int, to: Int, a0: Root): Root = {
+    var a = a0
+    var i = from
+    while (i < to) {
+      a = scanOp(a, Root(input(i), 1))
+      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/m20.md b/previous-exams/2021-midterm/m20.md
new file mode 100644
index 0000000000000000000000000000000000000000..59f6c09d903ebea6a9c020a96a176032387c186a
--- /dev/null
+++ b/previous-exams/2021-midterm/m20.md
@@ -0,0 +1,97 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+In this question, you will complete the definition of two concurrent data
+structures **without using `synchronized`**: `SeqCount` and `MultiWriterSeqCount`.
+
+## Part 1: SeqCount (defined in `SeqCount.scala`)
+
+An instance of this class stores two integers (initially set to 0), the stored
+values can be updated using `write` and retrieved using `copy`. Only one thread
+at a time is allowed to call `write` but multiple threads can call `copy` at
+once.
+
+Your task in this part is to implement `copy` such that this method never
+returns _partially updated values_, for example given two threads operating
+concurrently on a `SeqCount` `sc`:
+```scala
+// Thread 1
+sc.write(1, 2)
+```
+```scala
+// Thread 2
+val result = sc.copy()
+```
+`result` must either be `(0, 0)` (since the initial values are 0) or `(1, 2)`,
+but it must not be `(1, 0)`, `(0, 2)` or any other value.
+
+To successfully implement this method you will need to use `generation`: this
+method returns the current value of a volatile variable which is initially set
+to 0, gets incremented by one at the beginning of `write`, and incremented by
+one again at the end of `write`.
+
+**You are not allowed to use `synchronized` or directly call any of
+`myGeneration`, `myX` or `myY` (use the pre-defined getters and setters
+instead).**
+
+Hints:
+- Remember that a write to a volatile field _happens-before_ every subsequent
+  read of that field.
+- `generation` will always be even when a write has completed and always
+odd when a write is in progress.
+- `copy` can be implemented as a tail-recursive method.
+
+## Part 2: MultiWriterSeqCount (defined in `MultiWriterSeqCount.scala`)
+
+Like `SeqCount`, this class stores two integers updated using `write` and
+retrieved using `copy`, but unlike `SeqCount` multiple threads are allowed to
+call `write` at the same time: these writes will all succeed but they are
+allowed to complete in any order, for example given three threads operating
+concurrently on a `MultiWriterSeqCount` `msc`:
+```scala
+// Thread 1
+msc.write(1, 2)
+```
+```scala
+// Thread 2
+msc.write(10, 20)
+```
+```scala
+// Thread 3
+val result = msc.copy()
+```
+`result` must either be `(0, 0)`, `(1, 2)` or `(10, 20)`.
+
+In this class, the generation is stored in an atomic variable instead of a
+volatible field therefore it's important to note that:
+- a `set` on an atomic variable _happens-before_ every subsequent `get` of that
+  variable.
+- A call to `compareAndSet` both gets and set an atomic variable.
+
+Your task in this part is to implement both `copy` and `write`.
+
+**You are not allowed to use `synchronized` or directly call any of
+`myGeneration`, `myX` or `myY` (use the pre-defined getters and setters
+instead).**
+
+Hints:
+- you should be able to just copy-paste the implementation of `copy` you
+  implemented in Part 1
+- you will need to make use of `compareAndSetGeneration` in `write`
+- `write` can be implemented as a tail-recursive method.
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..b6bb8fe06564ac4c2bec2396c5dfe7caa3b51e44
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..89df220dfc550f774c094f0f8d5de2ae829009d5
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/MultiWriterSeqCount.scala
@@ -0,0 +1,66 @@
+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`.
+   */
+
+  @tailrec
+  final def write(newX: Int, newY: Int): Unit =
+    val old = generation
+    if old % 2 != 0 then
+      write(newX, newY)
+    else
+      if !compareAndSetGeneration(old, old + 1) then
+        write(newX, newY)
+      else
+        setX(newX)
+        setY(newY)
+        setGeneration(old + 2)
+
+  /** 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`.
+   */
+
+  @tailrec
+  final def copy(): (Int, Int) =
+    val old = generation
+    if old % 2 != 0 then
+      copy()
+    else
+      val result = (x, y)
+      if generation != old then
+        copy()
+      else
+        result
+
+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..ff95a2c7fd783adc96044752a0781c766d93ed9c
--- /dev/null
+++ b/previous-exams/2021-midterm/m20/src/main/scala/m20/SeqCount.scala
@@ -0,0 +1,53 @@
+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`.
+   */
+
+  @tailrec
+  final def copy(): (Int, Int) =
+    val old = generation
+    if old % 2 != 0 then
+      copy()
+    else
+      val result = (x, y)
+      if generation != old then
+        copy()
+      else
+        result
+
+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/m21.md b/previous-exams/2021-midterm/m21.md
new file mode 100644
index 0000000000000000000000000000000000000000..fb11db8278973e5a3f9f6a660a477e5f792f5bae
--- /dev/null
+++ b/previous-exams/2021-midterm/m21.md
@@ -0,0 +1,97 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+In this question, you will complete the definition of two concurrent data
+structures **without using `synchronized`**: `SeqCount` and `MultiWriterSeqCount`.
+
+## Part 1: SeqCount (defined in `SeqCount.scala`)
+
+An instance of this class stores two integers (initially set to 1), the stored
+values can be updated using `write` and retrieved using `copy`. Only one thread
+at a time is allowed to call `write` but multiple threads can call `copy` at
+once.
+
+Your task in this part is to implement `copy` such that this method never
+returns _partially updated values_, for example given two threads operating
+concurrently on a `SeqCount` `sc`:
+```scala
+// Thread 1
+sc.write(1, 2)
+```
+```scala
+// Thread 2
+val result = sc.copy()
+```
+`result` must either be `(0, 0)` (since the initial values are 0) or `(1, 2)`,
+but it must not be `(1, 0)`, `(0, 2)` or any other value.
+
+To successfully implement this method you will need to use `generation`: this
+method returns the current value of a volatile variable which is initially set
+to 1, gets incremented by one at the beginning of `write`, and incremented by
+one again at the end of `write`.
+
+**You are not allowed to use `synchronized` or directly call any of
+`myGeneration`, `myX` or `myY` (use the pre-defined getters and setters
+instead).**
+
+Hints:
+- Remember that a write to a volatile field _happens-before_ every subsequent
+  read of that field.
+- `generation` will always be odd when a write has completed and always
+even when a write is in progress.
+- `copy` can be implemented as a tail-recursive method.
+
+## Part 2: MultiWriterSeqCount (defined in `MultiWriterSeqCount.scala`)
+
+Like `SeqCount`, this class stores two integers updated using `write` and
+retrieved using `copy`, but unlike `SeqCount` multiple threads are allowed to
+call `write` at the same time: these writes will all succeed but they are
+allowed to complete in any order, for example given three threads operating
+concurrently on a `MultiWriterSeqCount` `msc`:
+```scala
+// Thread 1
+msc.write(1, 2)
+```
+```scala
+// Thread 2
+msc.write(10, 20)
+```
+```scala
+// Thread 3
+val result = msc.copy()
+```
+`result` must either be `(0, 0)`, `(1, 2)` or `(10, 20)`.
+
+In this class, the generation is stored in an atomic variable instead of a
+volatible field therefore it's important to note that:
+- a `set` on an atomic variable _happens-before_ every subsequent `get` of that
+  variable.
+- A call to `compareAndSet` both gets and set an atomic variable.
+
+Your task in this part is to implement both `copy` and `write`.
+
+**You are not allowed to use `synchronized` or directly call any of
+`myGeneration`, `myX` or `myY` (use the pre-defined getters and setters
+instead).**
+
+Hints:
+- you should be able to just copy-paste the implementation of `copy` you
+  implemented in Part 1
+- you will need to make use of `compareAndSetGeneration` in `write`
+- `write` can be implemented as a tail-recursive method.
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..879793e5498e751321315fff68817cea677fc435
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..73a81fb80a8cce594016ed1992be4fe246d2dab5
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/MultiWriterSeqCount.scala
@@ -0,0 +1,66 @@
+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`.
+   */
+
+  @tailrec
+  final def write(newX: Int, newY: Int): Unit =
+    val old = generation
+    if old % 2 != 1 then
+      write(newX, newY)
+    else
+      if !compareAndSetGeneration(old, old + 1) then
+        write(newX, newY)
+      else
+        setX(newX)
+        setY(newY)
+        setGeneration(old + 2)
+
+  /** 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`.
+   */
+
+  @tailrec
+  final def copy(): (Int, Int) =
+    val old = generation
+    if old % 2 != 1 then
+      copy()
+    else
+      val result = (x, y)
+      if generation != old then
+        copy()
+      else
+        result
+
+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..c17f45e56924d23b20cff009f8bfbf40a07fa4a0
--- /dev/null
+++ b/previous-exams/2021-midterm/m21/src/main/scala/m21/SeqCount.scala
@@ -0,0 +1,53 @@
+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`.
+   */
+
+  @tailrec
+  final def copy(): (Int, Int) =
+    val old = generation
+    if old % 2 != 1 then
+      copy()
+    else
+      val result = (x, y)
+      if generation != old then
+        copy()
+      else
+        result
+
+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/m3.md b/previous-exams/2021-midterm/m3.md
new file mode 100644
index 0000000000000000000000000000000000000000..ee3969ef97a4ed980c3d1cbbcfd1b60ced495a34
--- /dev/null
+++ b/previous-exams/2021-midterm/m3.md
@@ -0,0 +1,61 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+Given the following sequential implementation of a function that computes the sequence of rolling windowed averages, your task will be to complete and optimize a parallel version of this code.
+
+```scala
+/** 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)/n)`
+  */
+def rollingWinMeanParallel(arr: Arr[Int]): Arr[Double] = {
+  // Transform all numbers to averages of window of size 1
+  val arr1 = arr.map(x => AvgWin(x :: Nil))
+  // Compute the rolling windowed average by combining the windows together
+  val arr2 = arr1.scan(AvgWin(Nil))((acc, x) => acc.pushAll(x))
+  // Transform the windowed averages to Doubles
+  arr2.map(win => win.toDouble)
+  // Drop the extra initial element that was added by the scan
+  arr3.tail
+```
+
+ This implementation has some issues:
+ - It does not use parallelism
+ - Creates two intermediate arrays by calling `map`
+ - Creates an extra intermediate arrays by calling `tail`
+ - Scan returns an extra element we do not need
+
+ We want to parallelize and avoid the creation of the extra arrays.
+ As we are calling a `scan` the natural operations we need are `upsweep` and `downsweep`.
+ It is possible specialize those operations for our problem by letting those operations do the mapping.
+ It is also possible to change those operations to not generate the first element.
+
+We give you a version of `rollingWinMeanSequential` that partially implements the parallelization using `upsweep` and `downsweep`.
+
+ Your tasks in the exercise will be to:
+ - TASK 1: Implement the parallelization of `upsweep` and `downsweep`
+ - TASK 2: Remove the calls to the `map`
+ - TASK 3: Remove the call to `tail`
+
+ You can get partial points for solving part of the tasks.
+ The order of the tasks is a suggestion, you may do them in any order if that is simpler for you.
+
+Look at the `Lib` trait to find the definitions of functions and classes you can use (or already used).
+In this question we use a `Arr` array class instead of the normal `Array`. You may assume that this class has the same performance characteristics as the normal array. `Arr` provides only a limited set of operations.
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..dea8c3895d7df93482a29b4f7e0c85a5f13b7c12
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..81aff199d6413684f494ca01dacd7c0f470b654d
--- /dev/null
+++ b/previous-exams/2021-midterm/m3/src/main/scala/m3/M3.scala
@@ -0,0 +1,75 @@
+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)/n)`
+   */
+  def rollingWinMeanParallel(arr: Arr[Int]): Arr[Double] = {
+    if (arr.length == 0) return Arr.ofLength(0)
+
+    val out: Arr[Double] = Arr.ofLength(arr.length)
+    val tree = upsweep(arr, 0, arr.length)
+    downsweep(arr, AvgWin(Nil), tree, out)
+    out
+  }
+
+  // No need to modify this
+  def scanOp(acc: AvgWin, x: AvgWin) =
+    acc.pushAll(x)
+
+
+  def upsweep(input: Arr[Int], from: Int, to: Int): TreeRes = {
+    if (to - from < THRESHOLD)
+      Leaf(from, to, reduceSequential(input, from + 1, to, AvgWin(input(from) :: Nil)))
+    else {
+      val mid = from + (to - from)/2
+      val (tL, tR) = parallel(
+        upsweep(input, from, mid),
+        upsweep(input, mid, to)
+      )
+      Node(tL, scanOp(tL.res, tR.res), tR)
+    }
+  }
+
+
+  def downsweep(input: Arr[Int], a0: AvgWin, tree: TreeRes, output: Arr[Double]): Unit = {
+    tree match {
+      case Node(left, _, right) =>
+        parallel(
+          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[Int], from: Int, to: Int, a0: AvgWin, output: Arr[Double]): Unit = {
+    if (from < to) {
+      var i = from
+      var a = a0
+      while (i < to) {
+        a = scanOp(a, AvgWin(input(i) :: Nil))
+        output(i) = a.toDouble
+        i = i + 1
+      }
+    }
+  }
+
+
+  def reduceSequential(input: Arr[Int], from: Int, to: Int, a0: AvgWin): AvgWin = {
+    var a = a0
+    var i = from
+    while (i < to) {
+      a = scanOp(a, AvgWin(input(i) :: Nil))
+      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/m6.md b/previous-exams/2021-midterm/m6.md
new file mode 100644
index 0000000000000000000000000000000000000000..f0eb44964732a65fc946f8e095ad1948048799ac
--- /dev/null
+++ b/previous-exams/2021-midterm/m6.md
@@ -0,0 +1,155 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+In this exercise, you will implement an array Combiner using internally a doubly linked list of arrays. Your goal is to complete the implementation of the (simplified) Combiner interface, by implementing the `result` method to compute the result array from this array combiner.
+
+Here you can see the declaration of the `DLLCombiner` class and the related `Node` class definition. Look at the `Lib` trait in the `lib.scala` file to find all definitions of relevant functions and classes.
+
+
+```scala
+  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)
+```
+
+`DLLCombiner` class contains the implementation of methods `+=` and `combine`. You should look at them to better understand the structure of this array Combiner, before moving on to solving this exercise.
+
+Your task in the exercise will be to implement the `result` method of the `DLLCombinerImplementation` class. This method should compute the result array from this array combiner. This method should work in parallel according to the Combiner contract. Implement this method efficiently using 4 parallel tasks, by copying the doubly linked list to the array from both ends at the same time. Two threads should start from the start of the list and two from the end. In each case, one thread would be responsible for odd list indexes and the other for even ones.
+
+
+```scala
+  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]) = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the second, up to the middle
+    def task2(data: Array[Int]) = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the second to last, up to the middle
+    def task3(data: Array[Int]) = 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]) = {
+      ???
+    }
+
+    def result(): Array[Int] = {
+      val data = new Array[Int](cnt)
+
+      ???
+
+      data
+    }
+
+  }
+```
+
+Following the description above, your task in the exercise is to:
+
+- Implement the four tasks to copy parts of the array. Each task is responsible for computing one quarter of the array, as indicated in comments:
+    + Task 1: Computes every other Integer element of data array, starting from the first (index 0), up to the middle
+    + Task 2: Computes every other Integer element of data array, starting from the second, up to the middle
+    + Task 3: Computes every other Integer element of data array, starting from the second to last, up to the middle
+    + Task 4: Computes every other Integer element of data array, starting from the last, up to the middle
+- Implement the method `result` to compute the result array in parallel using those four tasks.
+
+Hints:
+
+- Note that this doubly linked list implementation uses `null` pointers to represent the absence of nodes. Be careful when working with `null` pointers in your solution, to avoid causing a `NullPointerException`.
+
+- `DLLCombinerImplementation` comes with a private `copyForward` method that you can use in your solution:
+
+```scala
+  private def copyForward(data: Array[Int], curr: Node, from: Int, to: Int, limit: Int)
+```
+
+This method copies certain elements from a doubly linked list to an array, in a way that might be useful for this exercise.
+
+## Examples
+
+Here is one example of the `result` method with intermediate results:
+
+```scala
+    val combiner1 = DLLCombinerImplementation(4)
+    combiner1 += 7 // (7)
+    combiner1 += 2 // (7, 2)
+    combiner1 += 4 // (7, 2, 4)
+    combiner1 += 3 // (7, 2, 4, 3)
+    combiner1 += 9 // (7, 2, 4, 3) <-> (9)
+    combiner1 += 5 // (7, 2, 4, 3) <-> (9, 5)
+    combiner1 += 1 // (7, 2, 4, 3) <-> (9, 5, 1)
+
+    val res1 = combiner1.result() // (7, 2, 4, 3, 9, 5, 1)
+
+```
+In this example, `task1` was responsible for computing elements at indexes 0 and 2, `task2` for computing the element at index 1, `task3` for computing elements at indexes 5 and 3, and `task4` for computing elements at indexes 6 and 4.
+
+Here is another example with combining:
+
+```scala
+    val c1 = DLLCombinerImplementation(4)
+    c1 += 7 // (7)
+    c1 += 2 // (7, 2)
+    c1 += 4 // (7, 2, 4)
+    c1 += 3 // (7, 2, 4, 3)
+    c1 += 9 // (7, 2, 4, 3) <-> (9)
+    c1 += 5 // (7, 2, 4, 3) <-> (9, 5)
+    c1 += 1 // (7, 2, 4, 3) <-> (9, 5, 1)
+
+    val c2 = DLLCombinerImplementation(4)
+    c2 += 6 // (6)
+    c2 += 8 // (6, 8)
+    c2 += 5 // (6, 8, 5)
+    c2 += 1 // (6, 8, 5, 1)
+
+    val c3 = DLLCombinerImplementation(4)
+    c3 += 1 // (1)
+
+    c1.combine(c2).combine(c3) // (7, 2, 4, 3) <-> (9, 5, 1) <-> (6, 8, 5, 1) <-> (1)
+    val res = c1.result() // (7, 2, 4, 3, 9, 5, 1, 6, 8, 5, 1, 1)
+
+```
+
+You can look at the public tests to find more examples.
+
+In your solution you should only make changes to the `DLLCombinerImplementation` class. You are not allowed to change the file `lib.scala`. You can get partial points for solving parts of this exercise.
+
+
+
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..2127d4e28e16c94709e41b0e9a6bf7e9a5ad4b88
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..f754c4a6b71257d94cfed13c57be81ad792c5d87
--- /dev/null
+++ b/previous-exams/2021-midterm/m6/src/main/scala/m6/M6.scala
@@ -0,0 +1,149 @@
+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 {
+
+      var current = head
+      if(current != null) {
+        var i_from = 0
+        var i_to = 0
+        copyForward(data, current, i_from, i_to, cnt/2)
+      }
+    }
+
+    // Computes every other Integer element of data array, starting from the second, up to the middle
+    def task2(data: Array[Int]): ForkJoinTask[Unit] = task {
+
+      var current = head
+      if(current != null) {
+        var i_from = 1
+        var i_to = 1
+
+        if(i_from >= current.cnt) {
+          current = current.next
+          if(current != null) {
+            i_from = 0
+          }
+          else i_to = cnt/2 // to stop the loop
+        }
+
+        copyForward(data, current, i_from, i_to, cnt/2)
+      }  
+    }
+
+    // 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 {
+
+      var current = last
+      if( current != null) {
+        var i_to = cnt - 2
+        var i_from = current.cnt - 2
+        if(i_from < 0) {
+          current = current.previous
+          if(current != null) {
+            i_from = current.cnt - 1
+          }
+          else i_to = cnt/2 - 1 // to stop the loop
+        }
+
+        copyBackward(data, current, i_from, i_to, cnt/2)
+      }
+    }
+
+    // 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 = {
+
+      var current = last
+      if( current != null) {
+        var i_from = current.cnt - 1
+        var i_to = cnt - 1
+        copyBackward(data, current, i_from, i_to, cnt/2)
+      }
+    }
+
+    def result(): Array[Int] = {
+      val data = new Array[Int](cnt)
+
+      val t1 = task1(data)
+      val t2 = task2(data)
+      val t3 = task3(data)
+
+      task4(data)
+
+      t1.join()
+      t2.join()
+      t3.join()
+
+
+      data
+    }
+
+
+    private def copyBackward(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 == -1) {
+            current = current.previous
+            i_from = current.cnt - 1
+          }
+          else if(i_from == -2) {
+            current = current.previous
+            i_from = current.cnt - 2
+            if(current.cnt == 1) {
+              current = current.previous
+              i_from = current.cnt - 1
+            }
+          }
+        }
+        catch{
+          case e: Exception =>
+        }
+      }
+        
+    }
+    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/m7.md b/previous-exams/2021-midterm/m7.md
new file mode 100644
index 0000000000000000000000000000000000000000..899703e0c7cfc8212fc6c2f95688d204bbf1a014
--- /dev/null
+++ b/previous-exams/2021-midterm/m7.md
@@ -0,0 +1,155 @@
+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)
+  * [The API documentation of the Scala parallel collections](https://www.javadoc.io/doc/org.scala-lang.modules/scala-parallel-collections_2.13/latest/scala/collection/index.html)
+  * [The API documentation of the Scala standard library](https://www.scala-lang.org/files/archive/api/2.13.4)
+  * [The API documentation of the Java standard library](https://docs.oracle.com/en/java/javase/15/docs/api/index.html)
+
+**If you have issues with the IDE, try [reimporting the
+build](https://gitlab.epfl.ch/lamp/cs206/-/blob/master/labs/example-lab.md#ide-features-like-type-on-hover-or-go-to-definition-do-not-work),
+if you still have problems, use `compile` in sbt instead.**
+
+## Exercise
+
+In this exercise, you will implement an array Combiner using internally a doubly linked list of arrays. Your goal is to complete the implementation of the (simplified) Combiner interface, by implementing the `result` method to compute the result array from this array combiner.
+
+Here you can see the declaration of the `DLLCombiner` class and the related `Node` class definition. Look at the `Lib` trait in the `lib.scala` file to find all definitions of relevant functions and classes.
+
+
+```scala
+  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)
+```
+
+`DLLCombiner` class contains the implementation of methods `+=` and `combine`. You should look at them to better understand the structure of this array Combiner, before moving on to solving this exercise.
+
+Your task in the exercise will be to implement the `result` method of the `DLLCombinerImplementation` class. This method should compute the result array from this array combiner. This method should work in parallel according to the Combiner contract. Implement this method efficiently using 4 parallel tasks, by copying the doubly linked list to the array from both ends at the same time. Two threads should start from the start of the list and two from the end. In each case, one thread would be responsible for odd list indexes and the other for even ones.
+
+
+```scala
+  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]) = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the first (index 0), up to the middle
+    def task2(data: Array[Int]) = task {
+      ???
+    }
+
+    // Computes every other Integer element of data array, starting from the last, up to the middle
+    def task3(data: Array[Int]) = 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]) = {
+      ???
+    }
+
+    def result(): Array[Int] = {
+      val data = new Array[Int](cnt)
+
+      ???
+
+      data
+    }
+
+  }
+```
+
+Following the description above, your task in the exercise is to:
+
+- Implement the four tasks to copy parts of the array. Each task is responsible for computing one quarter of the array, as indicated in comments:
+    + Task 1: Computes every other Integer element of data array, starting from the second, up to the middle
+    + Task 2: Computes every other Integer element of data array, starting from the first (index 0), up to the middle
+    + Task 3: Computes every other Integer element of data array, starting from the last, up to the middle
+    + Task 4: Computes every other Integer element of data array, starting from the second to last, up to the middle
+- Implement the method `result` to compute the result array in parallel using those four tasks.
+
+Hints:
+
+- Note that this doubly linked list implementation uses `null` pointers to represent the absence of nodes. Be careful when working with `null` pointers in your solution, to avoid causing a `NullPointerException`.
+
+- `DLLCombinerImplementation` comes with a private `copyForward` method that you can use in your solution:
+
+```scala
+  private def copyForward(data: Array[Int], curr: Node, from: Int, to: Int, limit: Int)
+```
+
+This method copies certain elements from a doubly linked list to an array, in a way that might be useful for this exercise.
+
+## Examples
+
+Here is one example of the `result` method with intermediate results:
+
+```scala
+    val combiner1 = DLLCombinerImplementation(4)
+    combiner1 += 7 // (7)
+    combiner1 += 2 // (7, 2)
+    combiner1 += 4 // (7, 2, 4)
+    combiner1 += 3 // (7, 2, 4, 3)
+    combiner1 += 9 // (7, 2, 4, 3) <-> (9)
+    combiner1 += 5 // (7, 2, 4, 3) <-> (9, 5)
+    combiner1 += 1 // (7, 2, 4, 3) <-> (9, 5, 1)
+
+    val res1 = combiner1.result() // (7, 2, 4, 3, 9, 5, 1)
+
+```
+In this example, `task1` was responsible for computing elements at index 1, `task2` for computing elements at indexes 0 and 2, `task3` for computing elements at indexes 6 and 4, and `task4` for computing elements at indexes 5 and 3.
+
+Here is another example with combining:
+
+```scala
+    val c1 = DLLCombinerImplementation(4)
+    c1 += 7 // (7)
+    c1 += 2 // (7, 2)
+    c1 += 4 // (7, 2, 4)
+    c1 += 3 // (7, 2, 4, 3)
+    c1 += 9 // (7, 2, 4, 3) <-> (9)
+    c1 += 5 // (7, 2, 4, 3) <-> (9, 5)
+    c1 += 1 // (7, 2, 4, 3) <-> (9, 5, 1)
+
+    val c2 = DLLCombinerImplementation(4)
+    c2 += 6 // (6)
+    c2 += 8 // (6, 8)
+    c2 += 5 // (6, 8, 5)
+    c2 += 1 // (6, 8, 5, 1)
+
+    val c3 = DLLCombinerImplementation(4)
+    c3 += 1 // (1)
+
+    c1.combine(c2).combine(c3) // (7, 2, 4, 3) <-> (9, 5, 1) <-> (6, 8, 5, 1) <-> (1)
+    val res = c1.result() // (7, 2, 4, 3, 9, 5, 1, 6, 8, 5, 1, 1)
+
+```
+
+You can look at the public tests to find more examples.
+
+In your solution you should only make changes to the `DLLCombinerImplementation` class. You are not allowed to change the file `lib.scala`. You can get partial points for solving parts of this exercise.
+
+
+
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..4536552028df3dbab3c2d6fd1d7ae9f4f1322f75
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..021c01988a1b773f2fa8ad84758b9bff1225dcf8
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/project/plugins.sbt
@@ -0,0 +1,2 @@
+// addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
+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..d941f26111e8732e80c8208ec9251a5b37828b79
--- /dev/null
+++ b/previous-exams/2021-midterm/m7/src/main/scala/m7/M7.scala
@@ -0,0 +1,149 @@
+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 {
+
+      var current = head
+      if(current != null) {
+        var i_from = 1
+        var i_to = 1
+
+        if(i_from >= current.cnt) {
+          current = current.next
+          if(current != null) {
+            i_from = 0
+          }
+          else i_to = cnt/2 // to stop the loop
+        }
+
+        copyForward(data, current, i_from, i_to, cnt/2)
+      }
+    }
+
+    // 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 {
+
+      var current = head
+      if(current != null) {
+        var i_from = 0
+        var i_to = 0
+        copyForward(data, current, i_from, i_to, cnt/2)
+      } 
+    }
+
+    // Computes every other Integer element of data array, starting from the last, up to the middle
+    def task3(data: Array[Int]): ForkJoinTask[Unit] = task {
+
+      var current = last
+      if( current != null) {
+        var i_from = current.cnt - 1
+        var i_to = cnt - 1
+        copyBackward(data, current, i_from, i_to, cnt/2)
+      }
+    }
+
+    // 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 = {
+
+      var current = last
+      if( current != null) {
+        var i_to = cnt - 2
+        var i_from = current.cnt - 2
+        if(i_from < 0) {
+          current = current.previous
+          if(current != null) {
+            i_from = current.cnt - 1
+          }
+          else i_to = cnt/2 - 1 // to stop the loop
+        }
+
+        copyBackward(data, current, i_from, i_to, cnt/2)
+      }
+    }
+
+    def result(): Array[Int] = {
+      val data = new Array[Int](cnt)
+
+      val t1 = task1(data)
+      val t2 = task2(data)
+      val t3 = task3(data)
+
+      task4(data)
+
+      t1.join()
+      t2.join()
+      t3.join()
+
+
+      data
+    }
+
+
+    private def copyBackward(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 == -1) {
+            current = current.previous
+            i_from = current.cnt - 1
+          }
+          else if(i_from == -2) {
+            current = current.previous
+            i_from = current.cnt - 2
+            if(current.cnt == 1) {
+              current = current.previous
+              i_from = current.cnt - 1
+            }
+          }
+        }
+        catch{
+          case e: Exception =>
+        }
+      }
+        
+    }
+    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
+      }
+    }
+  }
+}