Last updated on

Autograded Exercises with Stainless

Welcome to a new exercise set of CS-214 — Software Construction!

As usual, ⭐️ indicates the most important exercises and questions and 🔥 indicates the most challenging ones.

You do not need to complete all exercises to succeed in this class, and you do not need to do all exercises in the order they are written.

To complete these exercises, you can download the scaffold code (ZIP).

We have prepared a set of exercises that will help you explore course concepts from previous weeks, while at the same time leveraging the Stainless software verifier under the hood. You will find Monday’s week 13 lecture Formal Verification helpful for these exercises.

The auto-prover is set up on our servers: you can run it by submitting your solutions on Moodle, just like for labs (but no worries! The score returned by the grader and the feedback are just for you: just like other exercises, they don’t impact your course grade in any way).

Autograding with Stainless

If you submit a solution, you will receive automated feedback based not only on tests, but also on formal verification against a set of solutions. In addition to testing, our autograder uses Stainless for formal equivalence checking mode.

Recall that, in contrast to testing, formal verification provides a guarantee that your programs are never wrongly classified as correct or incorrect, reducing the danger of misleading you. Compared to syntactic code comparison, this approach allows you to experiment with solutions beyond those envisioned by instructors.

Recommended Workflow: How to use the feedback.txt file

We recommend submitting your solutions and inspecting the generated feedback.txt file. In the case where your solution fails on some test, or our verifier disproves the correctness of your submission, you will get as feedback an example input where your solution does not return the expected result. We encourage you to iterate on your solution and resubmit until you fully solve each problem. In the case where the feedback file reports safety errors for helper functions that you write, you should consider adding require statements to those functions to ensure that the input parameters are valid.

Troubleshooting: How to use the logging.log file

The logging.log file provides more details on the proofs made by the Stainless autograder. You can inspect the report at the end of the log file, in the stainless summary sections, where you will find two tables indicating the list of properties that the autograder tries to prove. These properties can be preconditions, pattern matching exhaustiveness, termination checks, equivalence checks etc. There you will also find the outcome (valid, invalid, timeout, etc.) of the autograder for each of these properties. Timeout indicates that the autograder was neither able to prove nor disprove that a certain property holds. The log file also provides additional information in case some internal error occurs.

Notes on Stainless Subset of Scala

Stainless Autograder supports a subset of Scala, with a preference for purely functional constructs. This subset largely aligns with the concepts that we present in the course. However, there are some language constructs that are currently not supported. Here is a (non-exhaustive) list of unsupported language constructs together with recommendations on how to avoid them:

To check whether you are staying within the fragment supported by Stainless, you can download the Stainless release and run stainless on your program. You can use command-line options to adjust stainless behavior. Command line options start with a double dash sign --. Here are some of the most important ones:

Now that you know the essentials about Stainless and how to use this formal verifier, you can start with the exercises!

Review of Pattern Matching exercises

In this first section, we will review some of the exercises of week 3 exercise set on Pattern Matching. You can try to submit the solutions you wrote during week 3, and see if the autograder can detect any errors in your implementation!

Contexts

We will start by reviewing the Contexts exercise.

Remember that our contexts associate names (Strings) with values (Ints), and let us do the following:

Implement the following functions and try to submit them on Moodle:

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/patmat/EnumContextOps.scala

Tree Mapping and Tree Reducing

We will now work with Trees. For this exercise, you will work with the same tree definition as in week 3:

enum IntTree:
  case Leaf(value: Int)
  case Branch(left: IntTree, right: IntTree)

src/main/scala/patmat/IntTree.scala

You need to implement and submit the three following functions:

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/patmat/IntTreeOps.scala

Natural Numbers ⭐️

For this exercise, you will work with Natural Numbers.

Remember that a way to represent natural numbers is to use the following enum:

enum UnaryNat:
  case Zero
  case Succ(pred: UnaryNat)

src/main/scala/patmat/UnaryNat.scala

This exercise is divided into three independent parts that you will submit individually.

Conversion to/from integers

In this section, you will implement the following functions:

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/patmat/UnaryNatConv.scala

Operations on UnaryNat numbers

Now that we can convert integers into UnaryNat numbers, we want to be able to make simple operations on them. Implement the following functions to do so:

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/patmat/UnaryNatOps.scala

Parity checks

Finally, we could want to know the parity of a UnaryNat number. Implement the following functions to be able to know whether a UnaryNat number is odd or even:

  def isEven(n: UnaryNat): Boolean =
    ???

src/main/scala/patmat/UnaryNatParity.scala

  def isOdd(n: UnaryNat): Boolean =
    ???

src/main/scala/patmat/UnaryNatParity.scala

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/patmat/UnaryNatParity.scala

Context as Binary Search Trees

For this exercise, we will review how to work with context using binary search trees.

Remember that to improve the efficiency of lookups, insertions, and removals in contexts, we used binary search trees (BSTs). BSTs store bindings in a sorted manner, allowing faster operations compared to linear structures like linked lists. You can have a look at the complete description made in week 3 following this link.

Stainless Autograder do not support string comparaisons with operators like < or >. To simplifiy the exercise, the BST you will use is a bit different to the one of week 3: it maps string, as value, to integrers, as key (instead of mapping integers to strings).

Here is the new definition of BSTContext:

enum BSTContext:
  case Leaf
  case Branch(k: Int, v: String, l: BSTContext, r: BSTContext)

src/main/scala/patmat/BST.scala

We divided this exercise into two parts that need to be completed and submitted individually.

Basic operations

First, you need to implement basic operations made on BSTs:

The lookup function will return an instance of the enum FindResult which is very similar to the LookupResultenum:

enum FindResult:
  case Found(value: String)
  case NotFound

src/main/scala/patmat/BST.scala

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/patmat/BSTOpsBasic.scala

Rotations

Now, you will work on rotation of BSTs. Remember that operations are much more efficient if they run on a balanced tree, so rotations on trees can be very useful. The following diagram illustrates two rotations (left and right), with the root node colored in blue:

rotation
Illustration of rotation.

Implement the two rotation functions in the file BSTOpsRotate:

  def rotateLeft(tree: BSTContext): BSTContext =
    ???

src/main/scala/patmat/BSTOpsRotate.scala

  def rotateRight(tree: BSTContext): BSTContext =
    ???

src/main/scala/patmat/BSTOpsRotate.scala

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/patmat/BSTOpsRotate.scala

Drop

Let’s now implement new functions and try to submit them on Moodle to see the result of the autograder.

For this exercise, you should implement a function that takes a list of integers ls and an integer n as argument, and returns the list obtained by dropping every n-th element from ls. For example, the input (List(1, 2, 3, 4, 5, 6, 7, 8, 9), 3) should lead to the output List(1, 2, 4, 5, 7, 8). You need to implement this function recursively.

  def drop(ls: List[Int], n: Int): List[Int] =
    require(n >= 0)
    ???

src/main/scala/drop/Drop.scala

You are provided with a custom implementation of List, you must use it instead of Scala’s List.

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/drop/Drop.scala

Prime numbers ⭐️

Implement a function that takes a natural integer n as argument, and returns whether this integer is prime or not. For example, the input 7 should lead to the output true. As a helper, you get the implementation of the isqrt function.

  def isPrime(n: BigInt): Boolean =
    require(n >= 0)
    ???

src/main/scala/prime/Prime.scala

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/prime/Prime.scala

GCD ⭐️

Implement a function that takes two natural integers a and b as arguments, and returns the greatest common divisor of these two. For example, the inputs 24 and 16 should lead to the output 8.

  def gcd(a: Int, b: Int): Int =
    require(a >= 0 && b >= 0)
    ???

src/main/scala/gcd/GCD.scala

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/gcd/GCD.scala

Infix operator

In this exercise, we will implement some definitions for logical operations.

Except for the not operation, find a way to define the following logical operations (and, or, equ, xor, nor, nand, impl) as infix operators for Boolean type.

Then, write the definitions of those 8 operators in the most concise manner.

For example :

val a, b : Boolean = true
	res = a xor b

Should compile and return false when evaluating res.

def not(b: Boolean): Boolean = ???

extension (a: Boolean)

  infix def and(b: Boolean): Boolean = ???

  infix def or(b: Boolean): Boolean = ???

  infix def equ(b: Boolean): Boolean = ???

  infix def xor(b: Boolean): Boolean = ???

  infix def nor(b: Boolean): Boolean = ???

  infix def nand(b: Boolean): Boolean = ???

  infix def impl(b: Boolean): Boolean = ???

src/main/scala/infix/Infix.scala

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/infix/Infix.scala

Board Game ⭐️

The goal of this exercise is to implement a made-up board game inspired by the game Civilization VI. The world of boardgame is composed of hexagonal tiles arranged on a cylinder (that is, they wrap around on the X-axis). This is represented by the class WorldMap in defs.scala. The tiles of the world are stored in a one-dimensional list and are ordered from left to right, bottom to top.

A tile (defined in defs.Tile) is made of the following:

Tiles have yields (described as a non-negative number, in defs.scala) representing how valuable they are. There are 4 kinds of yields: food, production, coins and science. The base yield depends on the kind of the tile, and is increased by the presence of a feature or resource. It can be further increased by an exploitation. A tile may produce multiple kinds of yields. If it does not produce a yield of any kind, its value is zero.

Warm-up: Tile Access

Implement the tile extension method for WorldMap in BoardGame.scala that, for given x and y, returns the tile at that position. The bottom-left of the map is defined to be at the position (0, 0). Remember that the tiles are ordered from left to right, bottom to top and that the X-axis wraps around.

The following should hold:

val G = Tile(TileBase.FlatTerrain(BaseTerrain.Grassland))
val X = Tile(TileBase.HillTerrain(BaseTerrain.Desert))
val Y = Tile(TileBase.Mountain)
val Z = Tile(TileBase.Lake)
val tiles = (
        G :: Z :: G :: G :: G ::
      G :: G :: G :: Y :: G ::
    G :: X :: G :: G :: G ::
  G :: G :: G :: G :: G :: Nil()
)
val wm = WorldMap(tiles, 5, 4)
assert(wm.tile(1, 1) == X)
assert(wm.tile(6, 1) == X) // wraps around X-axis
assert(wm.tile(3, 2) == Y)
assert(wm.tile(13, 2) == Y)
assert(wm.tile(1, 3) == Z)

Task 1: Yield Calculation

We are interested in calculating the yields of a tile. The yields are computed by summing the individual yields from the terrain, the feature (if any), the resource (if any) and the construction (if any). The exception to this rule is if the tile has a district. In this case, the yields for that tile are zero. We instead compute its adjacency bonus, discussed in Task 2.

The rules for the terrain are as follows:

If a tile has a feature, the following yields are added:

For tiles with a resource, the total yields are increased by:

The rules for the construction are as follows:

Your task is to implement the yields extension method for Tile in BoardGame.scala according to the above rules. You may add extension methods for BaseTerrain, Feature and so on or auxiliary functions to help you implement Tile.yields.

Task 2: Adjacency Calculation

Districts also produce “yields” of their kind through their adjacency bonus. However, as swiftly mentioned in the previous task, the yield coming from the tile itself is nullified. The adjacency yields kind science for campuses, production for industrial zones and coins for commercial hubs. The adjacency bonus depends on the tiles or constructions adjacent to the district in question. The total adjacency bonus is the sum of all adjacent bonuses, rounded down.

All districts gain 0.5 of their respective yield for each adjacent city center or district. The adjacency bonus is further enhanced by the following:

Implement the adjacencyBonus function in BoardGame.scala, that, given a WorldMap and a position (x, y) calculates the adjacency bonus for the district placed at that tile. If there are no district, it should return 0. You are not allowed to use floating point (Double or Float): instead, the calculation should proceed by having all yields doubled before having the total sum being divided by two, rounded down.

Task 3: City Settlement

For this final task, we will implement the rule for settling down new cities in BoardGame.validCitySettlement. Given a WorldMap and a position (x, y), a city can be settled down on that tile if all these requirements are met:

For the last rule, we split the task into 3 parts.

Part A

We are first going to compute the list of all tiles situated at the ring radius from a tile at (x, y) in BoardGame.collectTilesInRing. If radius is zero, the tile at (x, y) should be returned. The traversal must happen clockwise and start at the top-right corner of the hexagon. If, during the traversal, a tile happens to be outside (in y) of the world, it should be skipped. Note that the precondition require(2 * radius < wm.width) ensures that all tiles in x are within bounds and are not repeated due to wrapping.

Part B

With the help of collectTilesInRing, implement BoardGame.allTilesWithinRadius that should collect all tiles within radius from a tile at (x, y). The tile at (x, y) must be included as well. The list of tiles must be ordered from inwards to outwards.

Part C and putting it all together

We are almost there! It remains to fill noOtherCitiesInRange that returns true if there are no cities within 2 tiles from (x, y) and false otherwise. Then, validCitySettlement may be completed.

Notes

You are only allowed to modify the file BoardGame.scala. You are provided with a custom implementation of List you must use instead of Scala’s List, as well as a custom implementation of Option you must use instead of Scala’s Option.

Scala’s match expressions let you handle multiple cases in a single case statement, for example:

  res match
    case Resource.Crabs => Yields(coins = 2)
    case Resource.Fish | Resource.Rice | Resource.Wheat => Yields(food = 1)

However, in this exercise, you should not use this feature, as it is not currently supported by our grader. Please copy the logic for each case separately instead:

  res match
    case Resource.Crabs => Yields(coins = 2)
    case Resource.Fish => Yields(food = 1)
    case Resource.Rice => Yields(food = 1)
    case Resource.Wheat => Yields(food = 1)

Once you are done, submit the following file to the Moodle assignment for this exercise:

src/main/scala/boardgame/BoardGame.scala