Some basic tips for Coding Assignments

Published Jun 29, 2017

Design

Step back, and take a high level view to get a good overall approach.

For example, in a first assignment that is an assembler: this is relatively complex software meant to consume human input, and turn it into MIPS machine code

100,000 ft view:

  • this is a translator, consuming text and outputing machine code
  • parsing input
  • output generation
  • intermediate data structures likely
  • traversal is linear: from top to bottom
    • the various lines of the input are all translated (once each, in some way) into output

A complexity:

  • the significant amount of error checking that should be done on the user input (the assembly program)
    • A missing label is an error

    • A duplicate label declaration is an error

    • Using a code label for data operation, e.g.

      L1:
      ...
        lw r1,L1(gp)

    • Using a data label for code operation, e.g.

        .code
        beq r0,r1,myGlobal
        .data
        myGlobal: .word 0

10,000 ft view (one approach of several possible):

  • use two linear passes
    • first defines label address, and generates intermediate data structures
    • second generates output from intermediate data structures
  • employ a number of intermediate data structures
    • labels, instructions

In a second assignment that is an emulator of MIPS cpu/instruction set: since the machine code is meant to be consumed by the machine hardware directly, this is designed to be so easy that the hardware can do it(!), and, it requires virtually no extra storage, no translation, no intermediate data structures, no labels.

100,000 ft view:

  • this is decode in a loop
  • traversal is in flow of control order
  • data structures
    • machine state: registers
    • memory: code & data
  • there is virtually no memory allocation required during emulation itself — this is necessarily so because hardware doesn't get to allocate memory to help it either before or during its execution. To one way of looking at it, it just fetches, decodes & executes one instruction at time (though some of that may overlap, e.g. in pipelined processors).

A complexity:

  • Emuluation is running a program within a program, so that means two programs running in one! The emulator is the first program; the emulator's input is the second program, which is the machine code program to be emulated. We need to deconflict inputs & outputs of the two programs that (because of the nature of emulation) are sharing one process and hence also sharing the same command line to invoke both their execution.

10,000 ft view:

  • once you have a decode loop, disassembly is practically free
    • disassembly is a bit of a red herring here, as it as first blush it might appeared as assembler exercise — just in the other direction (a translator taking machine code and outputting text) — but that is more complexity than needed for this, given the decode that is already needed for execution.
  • inappropriate to apply multi-pass algorithm here
  • no need to unpack the instruction bits to something more easily executed
    • MIPS machine code is already packed for easy execution

For help with the 100,000 ft view:

  • A student recently informed me of https://www.interviewcake.com. I had a look at the free sample, and believe this can be useful to help learn problem analysis and decomposition.
  • Also use me or another mentor, early to help with 100,000 ft view. Get help early for an approach if you need it, then work alone, instead of the other way around — this so you don't spend a lot of time coding unhelpful or unecessary approaches. And of course, get more help as needed...

Study the inputs & outputs of the assignment, particularly, the output, then the input, so you really know what the program is supposed to do.

Writing Code

Now it is time to translate your approach into code.

When possible use C# or Java, in these languages, the compiler and runtime will help you with better error messages.

Avoid JavaScript and C (and C++), in these you'll spend too much time on trivial problems.

Start with minimal framework for the program, and step thru it with the debugger.

Add small amounts of code, compile and step thru with the debugger.

Always use good indentation, make it a habit. (I like 4 spaces, that's enough to create a good visualization but not enough to push lines to the edge of the screen window. Two spaces for indentation doesn't create a sufficient visualization for me.)

Use blank lines to separate groups of related lines of code, so, especially between functions, though do this inside functions as well.

Always use {}'s as you're writing code, just assume you'll need them. Start with empty {} and insert code inside that is nicely indented, of course. If we are (ever) done writing/editing the code, we can remove them where that is possible, and, at most, sometimes I'll do that in a small function where the code is simple and clear and the extra {}'s actually create more clutter than they help with.

Otherwise, assume the code you are writing will change, and thus, keep the indentation good to help read the code you're changing, and, use {}'s by habit — so as to avoid nesting errors. Automatic formatters are usually really helpful here, so if your development environment offers one, learn how to use it toward the beginning of your class.

Make a habit of initializing local variables.

Write controls structures first, then insert code, just like {}'s, e.g. regarding adding case write

case x :
    break;

and then introduce code in between the case and the break; lines.

Use lots of local variables. Your favorite statement should be a variable declartion with an initializer.

Don't do too much work in a single line of code. If you see that, break it into one or more variable declarations with initializers. So, use simple statements that inter connect via local variables. Local variables are virtually free as far as execution goes, and, they simplify debugging tremendously.

The more complex the expression, the harder to debug. And you won't have the local variable to printf or view in the debugger. Also, if the program crashes or throws you have less information about why/what/where.

If you see this:

return call ( complex.expression [ index - cacluation ] << 2 ) + baseValue;

replicate the line, and change the first to instead introduce a local variable, and the second to instead use the local variable.

int rv = call ( complex.expression [ index - cacluation ] << 2 ) + baseValue;
return rv;

and then do it again:

int arg = complex.expression [ index - cacluation ] << 2;
int answer = call ( arg ) + baseValue;
return answer;

and so forth, using your judgement, which you'll learn as you realize what you need to see during debugging. Use meaningful variable names as best you can.

Comment the code you're writing first with lines of brief descriptive human language, then write code in between the comments. Use blank lines to separate sections. (Then, when its working, update the comments that are probably now incorrect!)

To some extent you should already have your basic data structures designed with your overall approach. However, we often iterate, alternating some design with some coding, and hence introducing new data structures as we go.

For basic implementation of methods and functions think in terms of control structuctures

  • loops
  • if-then-else
  • switch

Be aware of code path both splitting and joining in if-then-else, and switch. These are key places, to do things like adding program logic or debugging printf's.

Write loops as either iteratative loops, e.g.

foreach ( var x in collection ) {
}

or as stepped-for loops, e.g.

for ( int i = 0; i < count; i++ ) {
} 

or as infinite loops (and only then edit later if you feel it is necessary). Avoid while or do unless you are already sure that's exactly you'll need. (So called "infinite loops" are simple and allow termination conditions to be expressed as separate, independent statements rather than as a single (potentially complex and larger) boolean expression as used in the while and do loops.)

int num = 0;
for (;;) {
    // prompt for input
    printf ( "Enter input: " );
    gets ( line );

    // check for user wants to abort
    if ( line.equals ( "quit" ) ) {
        printf ( "Quitting at user request.\n" );
        exit ( 1 ); 
        // or instead return an indication to quit
        // depending on your application
    }

    // check for bad or blank input
    if ( user_input_is_bad ( line ) ) {
        printf ( "bad or blank input, try again\n" );
        continue;
    }

    // check for number
    int cnt = sscanf ( line, "%d", & num );
    if ( cnt != 1 ) {
        // the scanf did not succeed
        printf ( "not a number, try again\n" );
        continue;
    }
    
    // on good input, the loop may only execute once, and, this is fine!
    break; // when we get here the input is ok, so stop the loop
}

There is no need for this to be a while loop. If it were, some lines in the loop might have to be duplicated (e.g. before the loop), and, duplication is something we try to avoid. Also, the conditions that might cause looping (blank input, or not a number) are complex enough to do separately on their own rather than within the boolean expression of a while or do while.

Think in terms of inputs & outputs: what you have vs. what you need. This is also the basic function abstraction: given this, produce that. Use functions instead of repeating code. One place to fix a bug is better than two.

Think in terms of types (e.g. is it a collection, vs. singleton) then in terms of values/variables.

Check errors. Consider whether a function might detect any errors at all, and if it could and should, provide the structure or control flow for error paths, so that constant checking for errors will be more painless, as you have a reasonable thing to do when you detect them. When you consider error handling, this may alter the function return types and/or values, e.g. it may affect the way you want to declare and invoke the function.

Debugging

Code rarely works the first time you run it. Simple things are often missing even in languages that help us more.

So, you need to get good at a variety of debugging techniques, and be able to find bugs, fix them, and move on to the next one right away, because this is pretty iterative.

  • One approach is to introduce printf's or WriteLine's depending on your language and environment. You'll need a lot of them so use numbering or something to keep them straight:

    printf ("message 1: %... %...\n", ..., ... );
    printf ("message 2: %...\n", ... );

    Remember you're using simple statements interconnected by lots of local, right?
    So, that's what we print in the printf's — those local variables. Also use printf's to see your flow of control. Put them at the top of control strutuctes (loops, switch'es, case statements). Put them in the control structures; put them at the end of control structures where code paths join back together.

  • Use single stepping in a debugger to watch control flow and to watch variables get values. Remember you're using simple statements connected by variables right?

  • Always keep the program compiling.
    Remember you're adding small increments of code, then compiling, running, and debugging that new code. This helps a lot with debugging, since you know what is working vs. what is new.

Summary

Before or early on in your coursework:

When you have the flexibility to do so, choose a good language and development environment.

Seek out and learn the automatic code formatting tool in your development environment, if that is availble. For example, you can use control-k, followed by control-d in Visual Studio. In Eclipse, it might automatically reformat as you type.

Seek out and learn the two basics of your debugger: statement single step and variable inspection. (Later, you'll want to be able to set a break point and run to it, so you don't have to single step past code you know is working.)

During assignments:

Get a good approach or overall design before too much coding. Use code coding habits & style. Write simple lines of code connected by variables. Write small increments of code, and compile & run that.

Appendix

Common problems in C

  • uninitialized variables — get in the habit of initializing local variables
  • null pointers — if you return null you'll have to check for it; otherwise, try to return something meaningful instead
  • memory used after free'd — local arrays disappear after leaving a function
  • missing break statement — easy to do in C, lint tools might help
  • redirected output is lost / doesn't appear when program crashes — don't redirect to a file, or, pipe to tee to capture the file and see the console output as well. (And an alternative is to flush stdout on signals.)
Discover and read more posts from Erik Eidt
get started
Enjoy this post?

Leave a like and comment for Erik

1