CSE 542 Programming Guidelines

These guidelines are intended to help you learn and practice good software development habits, which in the long term can help you write more reliable and readable programs and contribute more effectively as part of a software development team. In the short term, developing these habits also can help you avoid some of the more common mistakes that may result in grading deductions.

A. Program Compilation, Execution, and Submission

    Problems with program compilation and execution will often result in major (5-10 point) deductions during grading, though for less severe concerns (e.g., a compiler warning that should have been fixed but does not indicate a deeper problem with the program), minor (1-3 point) deductions are possible.

  1. Read each studio and lab assignment carefully and completely, and make sure you have done everything the assignment asks you to do. A common reason for deductions on a programming assignment (or a studio being marked incomplete) is failure to complete one or more tasks required for the assignment.

  2. Make sure you submit all the files needed to evaluate your solution. Deductions will be made for files missing from the lab submission as of the deadline for the lab.

  3. Always submit a readme file (named, e.g., "readme.txt") that documents (1) what the submitted program does, (2) any places where your solution diverged from what was requested in the assignment, and (3) the design decisions you made in developing your code. Please also ask any remaining questions you may have, in your readme file, and feedback on your impressions of the lab (what was easy, what was difficult, suggestions, etc.) is welcome.

  4. Test your program thoroughly. Don't just run it a few times and assume it will work correctly when it is graded (using a set of tests designed for that purpose). Instead, think about the ways your program might fail, and then design additional tests to explore those cases. Think about the different kinds of inputs a user might provide: could certain inputs (or failure by the user to provide those inputs) result in a run-time error?

  5. Your program's main function must return a value of 0 on successful termination, and should return a unique non-zero value for each different kind of unrecoverable failure otherwise: please avoid using the same non-zero return value or the same usage or error message for different kinds of errors (this often results from "copy and paste" coding). When different return values are possible, it's also good practice to document in the code and in your readme file what each return value means through (1) giving each non-zero return value a descriptively named label (see the guidelines below about not using hard coded numbers); (2) providing comments in the code; and (3) describing the kind of error that occurred (and how to avoid it) in the usage message(s) and error message(s) the program generates.

  6. Always check for success or failure of any call to a function that can fail, and then take appropriate action if the function failed. If there is nothing useful to do if a function fails (besides propagating the error upward per the next guideline), you should please add a comment explaining why there is nothing useful to do if the function indicated failure instead of success.

  7. Make sure to allow the program call stack to unwind if at all possible, even under abnormal conditions: avoid using std::process::exit() and instead use a return statement that propagates an appropriate error (e.g., using a Result type or an ExitCode type). Using std::process::exit() to end the program in the middle of a function interferes with the normal graceful shutdown of the program, and the release of resources the program acquired during its execution. It's better to use return statements with appropriate values (including using the Result type to distinguish correct results from erroneous ones, though if so give comments why) to cause the program to unwind its call stack and return an appropriate value (indicating success or failure) from its main function.

  8. Avoid unnecessary format restrictions, e.g., in general your programs should tolerate blank lines and extra whitespace in input files, unless there is an overriding reason not to do so (in which case you should please provide a comment in the code and in the design discussion in your readme file explaining why not).

  9. Be careful with array and vector indexes and pointers, and ensure that expressions involving them produce a valid object.

  10. Don't use fixed length buffers when the lengths of inputs to them are highly variable or unknown. Instead, use Strings for variable length character strings, and use vectors for other variable length buffers.

  11. Debugging programs is part science and part art. The science lies in (1) the methodical characterization of program behavior; (2) the development of hypotheses about why certain behaviors are or are not happening; (3) making predictions based on those hypotheses; and (4) designing and conducting (sometimes simple) experiments to evaluate those predictions and the underlying hypotheses. The art lies in creating good hypotheses, predictions, and experiments that lead you quickly and reliably toward an answer, and in thinking creatively about what could happen. For more on this process, see the Wikipedia Scientific method page.

    One very helpful question to ask throughout the debugging process is "what was the last point where my program is known to be working correctly, and what was the first point where it is known to be working incorrectly?" A debugger like rust-gdb (which is installed with rustc in the Linux Lab machines) is an invaluable tool in partially automating this process.

  12. Every program whose correct behavior depends on the number, format, or values of the command line arguments provided to it should have a helpful "usage" message. The usage message should be printed out if erroneous command line arguments (or too many or too few) are provided to the program, and should show the correct format for command line arguments. Whenever the usage message is printed out, the program should then return a unique non-zero value as well.

B. Program Style and Documentation

    Problems with program style and documentation usually result in only minor deductions during grading unless they contribute to a larger issue with program correctness, but even minor deductions can add up quickly if there are a lot of them. In the longer term, practicing a consistent and high quality programming style will make your code more accessible to others (and to you after you've been away working on other code for a while), which is essential in modern team programming environments.

  1. Avoid using unsafe to get code to compile, and instead address the issue that is causing any compiler error or warning. If code must be designated unsafe (which shouldn't be necessary in this course except in a studio exercise that explicitly says to do so for purposes of exploration) then you must provide comments explaining why that is the case.

  2. Always put a comment at the start of each source or header file, with (1) the name of the file, (2) the names and e-mail addresses of the authors of the code in that file, and (3) a brief summary of what the file contains (for example, "Definitions for the namehash program, which computes a simple hash value based on program argument strings.")

  3. Use comments and whitespace generously. Whitespace is free and can help make code much more readable than if it is all crammed together. Comments should summarize what each non-trivial section of code does, and indicate why you designed it the way you did.

  4. Avoid leaving "commented out" code fragments in submitted code - remove these or use conditional compilation (e.g., via the #[cfg] attribute) to compile them in/out (by default they should be compiled out) and comment why leaving them in the code is appropriate (e.g., if you intend to use them for debugging purposes in a subsequent lab assignment).

  5. Avoid commenting on things that are already fairly obvious from the code itself (e.g., where a method begins or ends).

  6. Always give any non-trivial formula involving size, indexing, or aliasing arithmetic a comment explaining it. In general, any non-obvious code should have a comment giving an intuition of what it does in general terms (and even better explaining why and/or how it does it that way).

  7. Avoid using hard coded constants like 2 and instead use an appropriately typed and descriptively named constant, like the following:
         const MINIMUM_ARGS: usize = 2;
        
  8. Avoid suppressing warnings that are on by default. For example, instead of using #[allow(dead_code)] to suppress warnings about members of a struct not being read when relying heavily on derived implrementations (e.g., via the #[derive]) attribute, design so that all members of a stuct are used directly by the code you provide in its implementation or by the code that uses the struct. In selected cases where suppressing a warning is the most reasonable thing to do (e.g., if you cannot guarantee code that uses a struct will necessarily access all its members, and relying on a derived implementation is appropriate), you must provide comments in the code and in your accompanying documentation (i.e., your readme file) explaining why it was appropriate to suppress that warning.

  9. Avoid using hard-coded numbers to index elements of a tuple, array, or slice by instead using match to destructure it to obtain one or more descriptively named references from it. For example, rather than writing:
          if current_part != t.1 {
               println!("");
               println!("{}.", t.1);
               current_part = t.1.clone();
          }
    

    instead say:

          match t {
              // destructure the tuple to obtain its part name
              (.., part_name, ..) => {
                  if current_part != *part_name {
                      println!("");
                      println!("{}.", part_name);
                      current_part = part_name.clone();
                  }
              }
          }
    

  10. Except where they are obviously appropriate (e.g., using "Error" in an error message) avoid including widely used character sequences in other places (e.g., using "Error" in a source or header file's name) to make your code files easier to work with on different platforms and in different development environments. For example, it's much easier to search for errors if only error messages (and not file names, etc.) contain "Error".

  11. Use the most natural control flow expressions in your code's logic. For example, use a loop expression for a non-terminating iteration (or one that should be exited via a break or return statement), instead of using e.g., while (true). Similarly, put the most salient case in the if branch and the "otherwise" case in the else branch, rather than negating the conditions to order them the other way. Note that in some cases, Rust will issue a warning and will suggest the better alternative, at compile time.