CSCI 3310
Operating Systems

Bowdoin College
Spring 2021
Instructor: Sean Barker

Project 4 - Virtual Memory Pager

Assigned:Monday, April 19
Groups Due:Wednesday, April 21, 11:59 pm
Code Due:Wednesday, May 12, 11:59 pm
Writeup Due:Friday, May 14, 11:59 pm
Collaboration Policy:Level 1
Group Policy:Pair-optional (but recommended!)

In this project, you will implement part of a virtual memory paging subsystem. In particular, you will implement an external pager, which is a process that handles virtual memory requests for other application processes. This program will be analogous to the core virtual memory components of an operating system that uses paging.

Pager Overview

Your pager will handle address space creation and destruction, page faults, and simple argument passing between virtual and physical address spaces. The pager will manage a fixed range of the virtual address space (called the arena) of each application that uses it. While running, the pager will handle all access requests to virtual memory addresses located in the arena. Valid pages in the arena will be stored in (simulated) physical memory or in (simulated) disk. Your pager will manage these two resources (physical memory and disk space) on behalf of all applications using the pager.

In addition to handling page faults, your pager will provide two system calls to applications: vm_extend and vm_syslog. An application uses vm_extend to ask the pager to make another virtual page of its arena valid. You can think of vm_extend like a very low-level memory allocation routine, on top of which a higher-level allocation library like malloc could be built (though your test applications will be calling vm_extend directly). The vm_syslog function is used to ask the pager to print a message in memory that exists in the virtual address space of the calling process (which may seem trivial at first, but is actually quite tricky)!

As a matter of interest, the vm_extend function is roughly equivalent to the real-world sbrk system call in Linux (which is used internally by malloc or new to enlarge the usable size of the heap).

Infrastructure Overview

Your external pager works in tandem with the CPU's hardware memory management unit (MMU) and exception-handling mechanism. The hardware MMU is automatically invoked on every virtual memory access and performs the following tasks:

  1. For accesses to non-resident or protected memory (explained later), the MMU triggers a fault, which transfers control to the kernel's fault handler, then retries the faulting instruction after the fault handler finishes. Note that a regular page fault (i.e., triggered by accessing a non-resident page) is one type of a fault but not the only one (the other type is a protection fault, which is a more general kind of fault described later).
  2. For resident memory accesses that are allowed by the page's protection settings, the MMU translates the virtual address to a physical address and accesses that physical address. This sequence does not involve the pager.
  3. Some MMUs automatically maintain reference and dirty bits, while other MMUs leave this task to be handled in software. The MMU in this project does not automatically maintain reference or dirty bits, so your pager will need to maintain this information.

Both faults and system calls invoke the exception mechanism. When a system call instruction is executed, the exception mechanism transfers control to the registered kernel handler for the exception.

The MMU and exception functionality in this project are emulated through a provided software infrastructure. To use this infrastructure, each application that uses the external pager includes vm_app.h and links with libvm_app.a, while your external pager itself includes vm_pager.h and links with libvm_pager.a. You do not need to understand the mechanisms used to emulate the hardware components (but in case you're curious, the infrastructure uses mmap, mprotect, signal handlers, named pipes, and remote procedure calls; suffice to say, there's a lot going on to simulate the regular hardware features of the system in a relatively seamless way).

Linking with these libraries enables application processes to communicate with the pager process in the same way that applications on real hardware communicate with the operating system. Specifically, applications issue load and store instructions (i.e., reads and writes to memory), and these instructions are translated or faulted by the infrastructure exactly as in the above description of the MMU. For faulting instructions and system calls, the infrastructure transfers control to the external pager via function calls.

The following diagram shows how your pager will interact with applications that use the pager. An application makes a request to the system via the system (function) calls vm_extend, vm_syslog and vm_yield, or by trying to load or store an address that is non-resident or protected.


                        initialize VM system         -->    vm_init
                        create process               -->    vm_create
                        end process                  -->    vm_destroy
                        switches to new process      -->    vm_switch

vm_yield         -->    may switch to new process
vm_extend        -->    system call handler          -->    vm_extend
vm_syslog        -->    system call handler          -->    vm_syslog
faulting load    -->    exception handler            -->    vm_fault
faulting store   -->    exception handler            -->    vm_fault

+-----------+           +----------------+                +----------------+
|APPLICATION|           | INFRASTRUCTURE |                | EXTERNAL PAGER |
+-----------+           +----------------+                +----------------+

Note in the above diagram that there are two versions of vm_extend and vm_syslog: one for applications and one for the pager. The application-side versions of these functions are implemented in libvm_app.a and are called by the application process. The pager-side versions of these functions are implemented by you in your pager. You can think of the application versions as system call wrappers, while the pager versions are the OS code that is invoked by making the system calls. When the application calls the wrapper functions, the infrastructure takes care of invoking vm_extend or vm_syslog in your page. The actual declarations for these functions are given in vm_app.h (for the application) and vm_pager.h (for the pager) and are described later in this writeup.

Address and Page Table Specification

A virtual address is composed of a virtual page number and a page offset, as follows:


         bit 63-13              bit 12-0
   +----------------------+-------------------+
   | virtual page number  |   page offset     |
   +----------------------+-------------------+

The simulated MMU uses a single-level, fixed-size page table. A page table is an array of page table entries (PTEs), one PTE per virtual page in the arena. The MMU locates the active page table through the page table base register (PTBR). In this case, rather than an actual hardware register, the PTBR is a global variable that is declared and used by the infrastructure, but will be controlled by your pager. The following portion of vm_pager.h details the arena, page table, PTEs, and PTBR.

/*
 * ***********************
 * * Definition of arena *
 * ***********************
 */

/* page size (in bytes) for the machine */
#define VM_PAGESIZE 8192

/* virtual address at which application's arena starts */
#define VM_ARENA_BASEADDR    ((void*) 0x60000000)

/* virtual page number at which application's arena starts */
#define VM_ARENA_BASEPAGE    ((uintptr_t) VM_ARENA_BASEADDR / VM_PAGESIZE)

/* size (in bytes) of arena */
#define VM_ARENA_SIZE    0x20000000

/*
 * **************************************
 * * Definition of page table structure *
 * **************************************
 */

/*
 * Format of page table entry.
 *
 * ppage: the physical page (frame) for this virtual page, if applicable.
 * read_enable: bit determining whether loads to this virtual page will fault.
 *    (0 ==> fault, 1 ==> no fault)
 * write_enable: bit determining whether stores to this virtual page will fault.
 *    (0 ==> fault, 1 ==> no fault)
 */
typedef struct {
    unsigned long ppage : 51;   /* bits 0-50 of pte */
    unsigned int read_enable : 1; /* bit 51 of pte */
    unsigned int write_enable : 1;  /* bit 52 of pte */
} page_table_entry_t;

/*
 * Format of page table.  Entries start at virtual page VM_ARENA_BASEPAGE,
 * i.e. ptes[0] is the page table entry for virtual page VM_ARENA_BASEPAGE.
 */
typedef struct {
    page_table_entry_t ptes[VM_ARENA_SIZE / VM_PAGESIZE];
} page_table_t;

/*
 * MMU's page table base register.  This variable is defined by the
 * infrastructure, but it is controlled completely by the student's pager code.
 */
extern page_table_t* page_table_base_register;

Make sure you understand the behavior of the fields of the page table entries. Whenever a page is accessed by the MMU, it is accessed either as a load (i.e., read) or a store (i.e., write). The two protection bits read_enable and write_enable determine whether reads or writes (respectively) will call vm_fault. Importantly, note that a non-resident page access (i.e., a regular page fault) is not the only time when you might need an attempted page access to fault. In other words, one or both protection bits might be sometimes set to 0 even in the case of a resident page. Clearly, a non-resident page would always need both protection bits to be set to 0.

If the protection bits do not trigger a fault, the MMU will access the ppage field (the frame/physical page number) and then access the requested memory address. For faulting accesses, the MMU will automatically retry the access after handling the fault via vm_fault.

A physical page (frame) may be associated with at most one virtual page at any given time (i.e., no sharing is allowed).

Client Application Interface

Applications use three system calls to communicate explicitly with the simulated operating system: vm_extend, vm_syslog, and vm_yield. The declarations for these system calls are given in vm_app.h:

/*
 * vm_extend
 *
 * Ask for the lowest invalid virtual page in the process's arena to
 * be declared valid.  Returns the lowest-numbered byte of the newly
 * valid virtual page.  For example, if the valid part of the arena
 * before calling vm_extend is 0x60000000-0x60003FFF, the return value
 * will be 0x60004000, and the resulting valid part of the arena will
 * be 0x60000000-0x60005FFF. The newly-allocated page is initialized to
 * all zero bytes. Returns null if the new page cannot be allocated.
 */
extern void* vm_extend();

/* 
 * vm_syslog
 *
 * Ask external pager to log a message of nonzero length len. Message data
 * must be in the part of the address space controlled by the pager.
 * Returns 0 on success or -1 on failure.
 */
extern int vm_syslog(void* message, unsigned len);

/* 
 * vm_yield
 *
 * Ask operating system to yield the CPU to another process. The
 * infrastructure's scheduler is non-preemptive, so a process runs
 * until it calls vm_yield or exits.
 */
extern void vm_yield();

The following is a sample application program that uses the external pager. This application allocates one virtual page within the arena, writes five bytes to it, then asks the pager to log the five bytes just written. Note that since vm_syslog is asking the pager to log the specified message, it doesn't result in any output from the application process.

// sample.cc - a sample application program that uses the external pager

#include "vm_app.h"

int main() {
    char* p;
    p = (char*) vm_extend(); // p is an address in the arena
    p[0] = 'h';
    p[1] = 'e';
    p[2] = 'l';
    p[3] = 'l';
    p[4] = 'o';
    vm_syslog(p, 5); // pager logs "hello"
    return 0;
}

Pager Specification

The functions that you must implement in your pager are declared in vm_pager.h. These declarations are shown below. Note you will not implement a main function; instead, main is included in libvm_pager.a and included in your pager during compilation. The infrastructure will invoke your pager functions as described previously.

/*
 * vm_init
 *
 * Initializes the pager and any associated data structures. Called automatically
 * on pager startup. Passed the number of physical memory pages and the number of
 * disk blocks in the raw disk.
 */
extern void vm_init(unsigned memory_pages, unsigned disk_blocks);

/*
 * vm_create
 *
 * Notifies the pager that a new process with the given process ID has been created.
 * The new process will only run when it's switched to via vm_switch.
 */
extern void vm_create(pid_t pid);

/*
 * vm_switch
 *
 * Notifies the pager that the kernel is switching to a new process with the
 * given pid.
 */
extern void vm_switch(pid_t pid);

/*
 * vm_fault
 *
 * Handle a fault that occurred at the given virtual address. The write flag
 * is 1 if the faulting access was a write or 0 if the faulting access was a
 * read. Returns -1 if the faulting address corresponds to an invalid page
 * or 0 otherwise (having handled the fault appropriately).
 */
extern int vm_fault(void* addr, bool write_flag);

/*
 * vm_destroy
 *
 * Notifies the pager that the current process has exited and should be
 * deallocated.
 */
extern void vm_destroy();

/*
 * vm_extend
 *
 * Declare as valid the lowest invalid virtual page in the current process's
 * arena. Returns the lowest-numbered byte of the newly valid virtual page.
 * For example, if the valid part of the arena before calling vm_extend is
 * 0x60000000-0x60003FFF, vm_extend will return 0x60004000 and the resulting
 * valid part of the arena will be 0x60000000-0x60005FFF. The newly-allocated
 * page is allocated a disk block in swap space and should present a zero-filled
 * view to the application. Returns null if the new page cannot be allocated.
 */
extern void* vm_extend();

/*
 * vm_syslog
 *
 * Log (i.e., print) a message in the arena at the given address with the
 * given nonzero length. Returns 0 on success or -1 if the specified message
 * address or length is invalid.
 */
extern int vm_syslog(void* message, unsigned len);

More details on the proper behavior of vm_fault and vm_syslog are provided below.

Page Replacement

If a fault occurs on a virtual page that is not resident, you must find a physical page (frame) to associate with the virtual page. If there are no free physical pages, you must create a free physical page by evicting a virtual page that is currently resident.

The pager must use the second-chance (clock) algorithm to select a victim. The clock queue is an ordered list of all valid, resident virtual pages in the system (i.e., global replacement). To select a victim, check the next physical page in the queue. If it has been accessed in any way since it was last, continue searching to the next page in the queue (and clear its reference bit).

If the next physical page in the queue has not been accessed, then its virtual page should be evicted. Dirty and clean pages are treated the same when selecting a victim page to evict (i.e., don't continue searching past a dirty eviction candidate in order to locate a clean candidate). Additionally, you should not write out a dirty page to disk unless you're actually evicting that page.

Syslog Behavior

The vm_syslog routine is called with a pointer to an array of bytes in the current process's virtual address space and the length of that array. The pager should first check that the entire message is in valid pages of the arena. Return -1 (and don't print anything) if any part of the message is not in a valid arena page, or if the message length is zero.

After checking the message validity, the pager should copy the entire message into a C++ string in the pager's address space, then print the C++ string to cout. You must use exactly the following formatting for your print statement (assuming your C++ string is named str):

    cout << "syslog \t\t\t" << str << endl;

You must treat access to the message by vm_syslog exactly the same as if the application had accessed the message itself (e.g., for purposes of residency, reference, and so forth). In other words, syslogging a message should produce the same behavior as if the application had accessed each byte of the message, starting from the lowest virtual address and proceeding towards the highest virtual address.

The print statement in vm_syslog should be the only output generated by your pager code. However, the pager infrastructure generates a significant amount of additional output itself (detailing the current operation of the pager). You can disable the infrastructure output during testing by passing the -q flag when running the pager.

Deferring Work

There are many points in this project where you have some freedom over when many types of work occur, such as zero-fills, faults, and disk I/O operations. You must defer such work as far into the future as possible (or even better, avoid it entirely). Performing appropriate work deferral is part of the required project specification, not simply a best practice. Deferring work to the maximum extent possible is one of the trickier parts of the project!

As a simple example of work deferral, if a page that is being evicted does not need to be written to disk, don't do so. However, make sure that you don't modify the page replacement algorithm (or any other aspect of the specification) in order to defer or avoid work.

There are cases where you might need to maintain extra state for the purpose of deferring or avoiding work. Carefully look for these cases!

If you could possibly defer or avoid some action at a cost of performing a different action, keep in mind the relative costs of various operations. Triggering a fault (about 5 microseconds) is cheaper than zero-filling a page (about 30 microseconds), which in turn is much cheaper than a disk I/O operation (about 10,000 microseconds). For instance, if you have a choice between incurring an extra fault or causing an extra disk I/O, you should take the extra fault.

Simulated Hardware Interface

This section describes how the external pager accesses simulated hardware (i.e. physical memory, disk, and the MMU).

Physical memory is structured as a contiguous collection of N phyiscal pages (frames), numbered from 0 to N-1. The number of physical pages is configurable via the -m command-line argument when executing the pager (e.g. by running ./pager -m 4). The minimum number of physical pages is 2, the maximum is 128, and the default is 4.

The disk is modeled as a single device that is a fixed number of "blocks" long, where each disk block is the same size as a physical memory page.

The pager controls the operation of the MMU by modifying the contents of the page tables and the page_table_base_register (PTBR) variable. Remember that although the MMU accesses the page tables automatically for the purpose of performing regular address translation, it is your pager's responsibility to allocate and maintain the page tables appropriately.

The following portion of vm_pager.h describes the variables and utility functions for accessing the disk and physical memory (the page tables and PTBR were shown previously):

/*
 * *********************************************
 * * Public interface for the disk abstraction *
 * *********************************************
 *
 * Disk blocks are numbered from 0 to (disk_blocks-1), where disk_blocks
 * is the parameter passed to vm_init.
 */

/*
 * disk_read
 *
 * Read the specified disk block into the specified physical memory page.
 */
extern void disk_read(unsigned block, unsigned ppage);

/*
 * disk_write
 *
 * Write the contents of the specified physical memory page onto the specified
 * disk block.
 */
extern void disk_write(unsigned block, unsigned ppage);

/*
 * ********************************************************
 * * Public interface for the physical memory abstraction *
 * ********************************************************
 *
 * Physical memory pages are numbered from 0 to (memory_pages-1), where
 * memory_pages is the parameter passed to vm_init.
 *
 * The pager accesses the data in physical memory through the variable
 * pm_physmem, e.g. ((char*) pm_physmem)[5] is byte 5 in physical memory.
 */
extern void* pm_physmem;

Note that unlike the page tables and PTBR, the pager is not responsible for initializing either the disk or physical memory. The pager is given the number of physical pages and disk blocks via vm_init, but it should not try to allocate actual disk blocks or physical memory space. Instead, it will work with disk_read, disk_write, and pm_physmem, which are already defined and initialized by the infrastructure. You should not make any assumptions about the starting contents of physical memory or the disk blocks.

Test Cases

As in the previous project, you will submit a suite of test cases that exercises your pager. Each test case for the pager will be a C++ program that uses the pager via the client interface defined in vm_app.h, and should be run without any arguments and without using any input files.

The filename of each test case must specify the number of physical memory pages to use when launching the pager that the test case will use. Specifically, the name of each test case must be of the format anyName.N.cc, where N is the number of physical memory pages. For example, you might name a test case myTest.4.cc. Remember that the minimum number of physical memory pages is 2 and the maximum number is 128.

Your test suite may contain up to 20 test cases. Each test case may cause a correct pager to generate at most 256 KB of output and must take less than 60 seconds to run (these limits are much larger than needed). You will submit your suite of test cases together with your pager to the autograder.

You should test your pager with both single and multiple application processes running. However, your submitted test suite need only consist of single process tests; none of the buggy pagers used to evaluate your test suite require multi-process applications to be exposed.

Finally, note that the autograder will check your test cases against the buggy pagers according to the pager (NOT the application) output. In other words, a test case exposes a buggy pager by causing the buggy pager to generate pager output that differs from the correct pager output. Of course, an application call to vm_syslog will directly correspond to a line of pager output, but the additional infrastructure output provides much more detail into the behavior of the pager. Since the only output your own pager code ever produces is the single line in vm_syslog, most of the output that is checked to expose the buggy pagers will be the infrastructure output that is indirectly generated by applications interacting with the pager.

Advice & Implementation Tips

General and specific advice on tackling the project is provided below.

Finite State Machine

One of the first things you should do is to write down a state-based flowchart (aka finite state machine) for the life of a virtual page, from creation via vm_extend to destruction via vm_destroy. Represent each virtual page as a series of bits (i.e., the relevant state of a page), where each state in the flowchart represents a specific setting of bits. You will, of course, need to decide what bits you need to represent each state. Some of these bits are straightforward (e.g., the page protection bits and the reference and dirty bits), but other bits may be needed as well. Ask yourself what events can happen to a page at each stage of its lifecycle (one example: the application reads the page) and what effect each such event will have on its state. As you design the state machine, try to identify all of the places where work can be deferred or avoided. This exercise may initially seem academic and unnecessary, but the correctness of your program will critically depend on correctly designing (and following) the state machine. I am happy to offer feedback on state machine designs!

One practical use of your state machine is to help in designing your test suite. For example, you can trace through different transition paths that a page can take through the state machine, then write a short test case that causes a page to take each path.

Faulting

One of the key mental hurdles in this project is understanding the full role of vm_fault within the pager. In particular, understand that vm_fault is not simply called when a non-resident page is accessed: while a regular page fault is one specific case in which a fault should occur, it is not the only case. Faulting is how your pager takes control from an active process and is given an opportunity to update internal state, perform any needed bookkeeping, etc. Thus, for any scenario where you need your pager to perform such tasks (even if not attempting to access a non-resident page), you will need to ensure that faults occur as needed by setting the page protection bits read_enable and write_enable accordingly.

Implementation Plan

When you are ready to begin coding, you should start with the basic pager functions that are automatically called by the infrastructure for any new process; namely, vm_init, vm_create, and vm_switch (there's also vm_destroy, but that's not initially essential). You will need to set up your core data structures within these functions.

Once these basic functions are working, move onto vm_extend and vm_fault, which will allow an application to actually make use of the arena memory.

Although it may seem counterintuitive, it is recommended to avoid tackling vm_syslog until most of the rest of the pager is working. You can pass several of the autograder tests without having implemented syslog at all (remembering that most of the output used to check your pager comes from the infrastructure itself, not from vm_syslog).

Pager Functions

Specific tips on each of the pager functions are provided below:

Debugging

For diagnostic printing in your pager, don't use cout, as any extra output sent to standard out (except for syslog) will cause your pager to fail test cases. Instead, print to standard error, i.e., using cerr instead of cout. This will allow you to leave active debugging statements when submitting to the autograder.

Liberal use of assertion statements is always a good idea to check for unexpected conditions generated by bugs.

Debugging in GDB is possible, but complicated somewhat by the impact of the infrastructure on the pager operation, particularly if the pager is missing basic functionality. At least initially, effective use of GDB may be limited until you've implemented the most basic parts of the pager (e.g., vm_init, vm_create and vm_switch).

Logistics

As usual, starter code will be distributed via GitHub. GitHub repositories will be made available once groups are assigned (each group will share one repository).

Write your pager code inside pager.cc and write each test case in a separate file named anyname.N.cc, where N is the number of physical memory pages to use for that test (which must be between 2 and 128). The included sample.4.cc is an example of a test case. The pager infrastructure is Linux-specific and will not run on any other system. Your program may use any functions in the standard C++ library, including (and especially) the STL. You should not use any libraries other than the standard C++ library.

The public functions in vm_pager.h are declared extern (meaning that code outside of the pager can call them), but all other functions and global variables in your pager should be declared static (meaning that code outside of the pager will not see them) to prevent naming conflicts.

Compiling and Running

Compiling and testing your pager requires compiling your pager code in pager.cc as well as an application program that uses the pager, such as sample.4.cc. Note that both the pager and the application program are separate executables, so they must be compiled and run separately.

You can compile the pager using the include Makefile by simply running make, which will output an executable named pager. The initial pager.cc file contains a dummy implementation of each required pager function, so you can compile the pager immediately. Separately, to compile an application program (or test case) named mytest.N.cc into an executable named mytest, you can run make mytest.N. You can substitute whatever test program name you wish (e.g., make sample.4 to build the sample test case).

To run your pager and an application, first start the pager (e.g., by running ./pager). Remember that you can suppress the regular infrastructure output by passing the -q flag and/or specify the number of physical memory pages via the -m flag (although the number of disk blocks is non-configurable). While the pager is running, you can then run one or more application processes that will interact with the pager. For example, in a separate window, you could run ./sample.4 to run the sample test case. The same user must run the pager and any applications that use the pager, and all processes must run on the same machine.

Sometimes the pager may get "stuck" and appear to still be running, but applications are unable to connect to it (this may happen if a buggy application previously crashed while using the pager). If an application appears to hang when started and cannot communicate with the pager, you may need to restart the pager process and try again.

Submitting to the Autograder

You can submit your program to the autograder as follows (the pager must be named pager.cc, but the test programs can be named whatever you wish in the appropriate format):

submit3310 4 pager.cc test1.4.cc test2.4.cc ...

To more easily submit all your source files at once (pager.cc and all of your test programs), you can use a shell wildcard, like so:

submit3310 4 *.cc

Feedback note: The nature of this project means that the autograder evaluation of your submission may take substantially longer than in past projects. Expect a finished and reasonably efficient pager to take roughly 10-15 minutes to complete the primary pager test suite (an incomplete, buggy, or inefficient pager may take more or less time). Evaluating your test cases will take additional time; given the number of buggy pagers, it is very possible for a submission that includes a large number of test cases to take 1-2 hours to complete testing. Be patient and plan accordingly! However, if you have not received autograder feedback within 4 hours of submission, there is likely something wrong and you should let me know.

Note that your submissions are timestamped according to when you submit them, not when the feedback email is generated (so if you submit at 11:55 pm, it will still count as that day's submission even if you don't get feedback until after midnight).

Writeup

In addition to writing the program itself, you will also write a short paper (~3-5 pages) that describes your pager. Your paper should include the following:

  1. a brief introductory section providing an overview of the project
  2. a design section that describes your major design choices and the data structures you used, focusing particularly on work deferral in the pager (if a figure makes your explanation more clear, use one!)
  3. an implementation section that overviews the structure of your code (at a reasonably high level - should not duplicate your code)
  4. an evaluation section that describes how you tested your pager
  5. a conclusion that summarizes your project and reflects on the assignment in general

Remember that the writeup is a supplement (not a substitute) for the code itself. Strive to minimize any degree to which your writeup is 'code-like'. Instead, you should approach the writeup like a technical paper (providing clear, prose explanations that can be supplemented by reading the code). You need not try to include every technical detail in the writeup; focus on the major challenges and design decisions as you might explain them to another computer science student.

Upload your writeup as a PDF to Blackboard by the writeup deadline. You only need to submit one copy of the writeup per team. Typesetting your writeup in LaTeX is encouraged but not required.

Evaluation

Your project will be graded on program correctness, design, and style, as well as the quality of your project writeup. Remember that the autograder will only check the correctness of your program, nothing else!

You can (and should) consult the Coding Design & Style Guide for tips on design and style issues. Please ask if you have any questions about what constitutes good program design and/or style that are not covered by the guide.