Machine Problem 6, due at the end of Nov 13

Part of the homework for CS:2630, Fall 2023
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

Top Level Specification: You must provide a callable function, FTOINT that takes an IEEE 32-bit floating point parameter in R3 and returns a 32-bit two's complement integer holding the value of that argument.

The conversion to integer should truncate toward zero. That is, the fractional part of the parameter should be simply discarded, with no attempt to round to the nearest integer.

For most purposes, your code will be equivalent to (albeit harder to write) this C code:

int ftoint( float f ) {
    return (int)f;
}

If the floating point value is larger than can be represented in 32 bits, the value returned should be 8000000016.

Your submission must not include a main program. The symbol FTOINT must be declared in your code as having an internal definition (that is, your code must contain a line saying INT FTOINT.

Your code must conform to the standard Hawk calling sequence conventions. We will link it with our main program to test it, and our program will assume that you obey the calling conventions.

The submitted version of your code should not include an S directive.

Discussion: Curiously, there is no need to use the Hawk floating point coprocessor in your FTOINT code. The floating point coprocessor does integer to floating conversion, but not floating to integer.

You must write your own main program to test your code. You may well want to use the floating-point coprocessor to do this. A bare minimum test program might be equivalent to this C code:

int main() {
    int i;
    for (i = 1000000000; i > 1; i = i/10) {
        if (i != ftoint( (float)i )) {
	    printf( "fails for i = %d\n", i );
	}
        if (-i != ftoint( (float)-i )) {
	    printf( "fails for i = -%d\n", i );
	}
    }
}

If you use the above code, beware of several things: First, it was written without regard to the limited precision of type float. We're using 32-bit IEEE format numbers, these only support 24 bits of precision. As a result, there is no exact representation for 1,000,000,000 in that system. It is a legal 32-bit integer, but convert it to float and back, and you'll get something similar that may be off by quite a bit.

Doing this test on the Hawk will involve using the floating-point coprocessor, since you need it to do integer to floating conversion (unless you want to double your work load and write your own).

The above test does not cover NaNs (not-a-number values), infinite values, fractions or floating-point values that are out of range. You'll probably want to test these.

Grading: 5 points. Correct output is worth 2.5 points. The remaining credit will be offered only for those who have reasonably correct output.

Code containing assembly errors will not earn any credit.

Your program must be written in SMAL Hawk assembly language, and it it must be formatted so that it does not trigger warnings when you run

   [HawkID@fastx?? ~]$ ~dwjones/format mp5.a

The final 2.5 points of the score will be based on program organization. Stylistically clean code is important; that is why half the credit is reserved for style, but only if the code works. Bad indenting of the assembly code itself will be penalized up to 1 point from the 2.5 style points. The use of indenting to improve comment readability is equally important. Excessive or inadequate white space within and between lines will be penalized, again, up to 1 point. Excessive or inadequate comments will be judged similarly.

Comments must show clear evidence of planning. Clear comments in a notation similar to C are very valuable. If the code does not do what the comments say, that will be penalized. Control structures in your code that cannot be expressed in a readable high-level language style of commentary will be penalized. Comments that do not reflect the actual control structure of your code will be penalized. When you edit your code, don't forget to update the comments.

Function headers are important. Do not deviate from the standard Hawk calling sequence rules. Document your activation records! Document the registers you use (but note: everyone should know that R1 and R3 are locally available between calls to functions.

Do not put off commenting your code! If you ask for help with your code and it is not properly commented, you will be told to write good comments first! This is because the exercise of writing good comments frequently helps you find out what your code really does, and even when it does not, the comments tell us what you thought your code was supposed to do.

If you base your code on examples from the notes, code distributed in previous course offerings, code distributed with lecture notes, or any source other than your own brain, say so! Cite your sources! Failure to cite sources will be penalized.

Submission: To submit your work, it must be in a file named mp6.a in your current directory on the CLAS Linux system, and you must know your section number. In the following, what you type is shown in bold face. Begin at the linux command-line prompt:

   [HawkID@fastx?? ~]$ ~dwjones/submit 0A0X mp6.a

Note: The ~dwjones must be verbatim, do not substitute another name. Also, use your section number (one of 0A01 to 0A05).

The submission script will copy your code and, after the copy is made, it will output a message saying "successful submission." You may resubmit as many times as you want; each time you resubmit, your previous submission of that file will be replaced, so we will only see your final submission.

In the event of insurmountable trouble, do not delete or edit files you have submitted until the graded work is returned. The operating system record of the last edit time and date and the backups maintained by the operating system are proof of what you did and when, and they allow us to investigate what happened. until your graded work is returned. This way, the time stamp marking the time of last edit is a trustworthy indicator of whether you did the work on time.


Discussion

Nov. 4, 6:29 PM

I submitted what I’m working on, with hopes that you can see what I am referring to with this. My comments outline my logic.

I looked at the code, and my immediate reaction is that you need to do some little experiments, single stepping through small code fragments. For example, your question about TRUNC can be answered by an experiment that takes less time to run than it takes to e-mail me ang get a reply. Here's my experimental code:

        USE     "hawk.h"
        LIS     R8,-1		; a value to truncate
        TRUNC   R8,4		; truncate it to see the effect

I assembled it. There's no need to link, since it doesn't use the operating system, so I just ran the assembled object file under the Hawk emulator and used the emulator's s command (single step) to run one instruction at time, looking at how R8 changes. Here's what I got:

R8: 00000000 -- before the first instruction R8: FFFFFFFF -- after the LIS R8: 0000000F -- after the TRUNC

In looking at your code, I see several places where similar trivial experments would help you understand your code.

At one point in your code, you made an interesting optimization. A C programmer might have written this to clear the sign bit:

abs = (*(int *)&x) & 0x80000000;
In assembly language, assuming the floating point value x is in R4, and you want that with the sign bit cleared in R3 this would be:
	LIW	R3,#80000000
	AND	R3,R4

Note that the LIW macro is really LIL plus ORIS and note that LIL is a long instruction. You did something like this instead:

	SL	R3,1
	SR	R3,1	;<---- small important mistake

That almost works, and in fact it's on the road to a good optimization. The problem is, you used SR a sign-preserving shift. What you should have written, and in fact, the recommended way to clean off bits from the top of a word, is something like this:

	SL	R3,1
	SRU	R3,1	;<---- an unsigned shift

Afterword: What was that in C about *(int *)&x?

C has a design flaw. If I write (int)x where x is a floating point number, it means, do the computations needed to truncate x to the nearest integer and give me an integer result. That is, it forces a change of representation as well as a change of type.

In contrast, if x is a non numeric type, and I write (int)x, it means: Take the bit pattern used to represent x and with no change in represention, pretend it's an integer.

What we wanted in our code above was to use an integer operation (logical and) on the representation of a floating point value. To do that in C, we first need to convince the compiler to change the type label on the value without changing its representation. So, we do something awkward:

&x — make a pointer to the variable x.

(int *)&x — pretend it's a pointer to an integer.

*(int *)&x — get the value of that integer.

Ugly, but if the compiler does any optimization, no pointer will ever be generated or followed and instead it will simply use the floating point value as if it was an integer.

Nov. 4, 9:58 PM

I resorted (on paper) to converting the mantissa into a integer (multiplying by 2^23). I'm not sure if this software can handle that sort of number though. I then have written down to add 1*2^23 if the value should be normalized. Then I multiply by 2^exp. Next I would divide back by 2^23.

I have the sense that this number would be too big for the hawk to handle.

First, you are right. All of the Hawk monitor arithmetic support is limited to 32 bits, as are the Hawk's shift instructions. It is entirely possible to write higher precision integer multiply and divide routines, or to write higher precision shift instructions, but there's nothing built in.

But note: You said multiply or divide by powers of two. You never need to use any multiplication or division to solve this problem. It's all done by shifting.

Section 10.3 and Section 11.2 of the Hawk manual discuss instructions that can be used to make 64-bit precision left and right shifts, but you don't need those tricks for this problem because it can all be done in 32 bits. If you do some algebra on all the multiplies and divides you discussed above, you will discover that your large constand multiplier and divisors cancel out!

Nov. 7, 9:27 AM

I noticed in mp6 that you are expecting the students to check for infinity and NaN. What should the subroutine return in these cases?

The assignment says: "If the floating point value is larger than can be represented in 32 bits, the value returned should be 8000000016."

Infinite values cannot be represented in 32 bits, so this obviously applies to them. NaN values seem to fit naturally in the same class.

Nov. 10, 8:27 AM

Several students noted that you can't write SL R3,R4 to shift R3 left a number of places specified by R4. What you can do is either:

a) Write a for loop repeatedly doing SL R3,1 the right number of times or

b) Look at the final question on Midterm II and use that algorithm to efficiently solve the problem.

Nov. 10, 6:45 PM

Debugging the shift counts in this code is messy, and I found it much easier to dig around in the logic working in C instead of assembly language. The challenge is that C doesn't pin down the size of types int and long int. As an afterthought, the header file stdint.h was introduced to define well defined fixed size integer types.

Design by afterthoughts always seems to produce ugly results, and that is certainly the case here. stdint.h includes two relevant types, int32_t for exactly 32-bit signed integers, and uint32_t for exactly 32-bit unsigned integers. Both of these are relevant here.

Actually, the precision of type float is equally undefined, but it is almost always IEEE 32-bit float, and I've tested that. It is true on our machine.

To help you experiment with these, here is the skeleton of my C solution to this problem:

int32_t ftoint( float f ) {
    uint32_t fasint = *((uint32_t*)&f);
    int32_t retval; /* the return value */
    
    /* pick apart the number */
    uint32_t s    = fasint & (uint32_t)0x80000000; /* 1 bit sign */
    uint32_t exp  = fasint & (uint32_t)0x7F800000; /* 8 bit exponent */
    uint32_t mant = fasint & (uint32_t)0x007FFFFF; /* 23 bit mantissa */

    /* your solution here */
    
    return retval;
}

Of course, after debugging the logic, I translated it to assembly language, where objects of type float, uint32_t and int32_t are all just values of words or registers.

Nov. 12, 3:17 PM

At first, I was going to use the AND instruction, but I ended up using shift operations. Am I correct in thinking that it does the exact same thing?

You can frequently use shifting instead of anding with a mask. That was the motivation behind some of last week's quiz/homework questions. So suppose you have a variable a that is a 32-bit unsigned integer (type uint32_t in C). You could write the expression:

a & 0x00FFFF00

or you could get the same result using

((a << 8) >> 16) << 8

Programmers frequently refer to the first approach, with and, as masking off or masking out the unwanted bits, while they refer to the second approach as shifting off or shifting out the unwanted bits. It is quite common to want to do some shifting anyway, as for example:

(a & 0x00FFFF00) >> 8

When this is the case, it is frequently faster to write:

(a << 8) >> 16

Good compilers tend to have lots of special cases in them to recognize cases where masking can be converted to shifting, so high level language programmers generally just write code with masking, and let the compiler substitute shifting out the unwanted bits. Assembly language programmers frequently go directly to shifting off unneeded bits.

Nov. 13, 9:30 AM

I have included a very simple main program loading R3 with the floating point value of 48 so you can easily run my program to see how it is functioning. When I comment out the main program, leaving only INT FTOINT, the activation record for FTOINT and everything below the FTOINT: line, I get this error ...

I quote the assignment:

Your submission must not include a main program. The symbol FTOINT must be declared in your code as having an internal definition (that is, your code must contain a line saying INT FTOINT.

That means that your file must not contain a line saying INT MAIN nor a line saing S MAIN.

While you may use a test program in the same source file during development, we will link our test program with the object file produced by assembling your code. Our test script works like this, where mp5.a is your code and mp5test.o is our main program to test your code:

smal mp6.a
hawklink mp6.o mp6test.o
hawk link.o

You should really test your code by assembling it and linking it to your own test program.