Introduction
Mixed arithmetic is the capstone of the assignment.
You seamlessly allow arithmetic and relational operators on a mix of small and large integers.
You extend small-integer arithmetic so that when an intermediate result doesn’t fit in a machine word, it automatically “fails over” to large-integer arithmetic.
You don’t define any new classes; instead, you use addSelector:withMethod:
to extend or modify existing classes.
Do not modify file bignum.smt
. Instead, get ready by creating a new, empty file mixnum.smt
. There is nothing to copy into the file.
Class SmallInteger
Implement promotion. To implement a binary method on combination of small and large integers, you first promote the small integer to large, then use the corresponding large-integer method. You will add a new method
asLargeInteger
to classSmallInteger
. (It is already defined on classLargeInteger
, where it answersself
.)The method on class
SmallInteger
is added using the reflection APIaddSelector:withMethod:
, by executing the following code at top level:(SmallInteger addSelector:withMethod: 'asLargeInteger (compiled-method () (... make a large integer from `self` ...)))
Complete the code and add it to your file
mixnum.smt
.Study
SmallInteger
. Allocating objects and sending messages to them gets you only so far. Eventually you have to get the machine to do things with bits. In \(\mu\)Smalltalk, the machine is invoked by a syntactic form with the keywordprimitive
. This form looks like a function call in Impcore: it names the primitive and gives one or more arguments.Study these excerpts from the definition of the
SmallInteger
class:(class SmallInteger [subclass-of Integer] ; primitive representation ... (method negated () (0 - self)) (method print () (primitive printSmallInteger self)) (method + (n) (primitive + self n)) (method - (n) (primitive - self n)) (method * (n) (primitive * self n)) (method = (n) (primitive sameObject self n)) (method < (n) (primitive < self n)) (method > (n) (primitive > self n)) )
You will use some of these same primitives in new methods that you will define. And later you will replace some of the primitives with more complicated primitives that detect overflow, as described in Exercise 39.
Multiplication
Implement mixed multiplication The laws for mixed multiplication say only what needs to be promoted. All the laws use the same notation:
\(I\) A large integer \(i\) A small integer \(P(i)\) A large integer obtained by promoting a small integer (sending asLargeInteger
)\(\mathbin{\cdot_{\scriptscriptstyle L}}\) Multiplication by a large integer \(\mathbin{\cdot_{\scriptscriptstyle S}}\) Multiplication by a small integer \(\mathbin{\cdot_{\scriptscriptstyle LL}}\) Multiplication of two large integers \(\mathbin{\cdot_{\scriptscriptstyle SS}}\) Multiplication of two small integers (a primitive!) \(\mathbin{\cdot}\) Multiplication not otherwise specified The \(\mathbin{\cdot_{\scriptscriptstyle SS}}\) operation, which multiplies two small integers, is the only primitive. Everything else is just allocating objects and sending messages to them.
The multiplication laws start with four laws which encode the double dispatch that method
*
must implement: \[\begin{aligned} i_1 \mathbin{\cdot}i_2 &= i_2 \mathbin{\cdot_{\scriptscriptstyle S}}i_1\\ i_1 \mathbin{\cdot}I_2 &= I_2 \mathbin{\cdot_{\scriptscriptstyle S}}i_1 \\ I_1 \mathbin{\cdot}i_2 &= i_2 \mathbin{\cdot_{\scriptscriptstyle L}}I_1 \\ I_1 \mathbin{\cdot}I_2 &= I_2 \mathbin{\cdot_{\scriptscriptstyle L}}I_1 \end{aligned}\] The remaining multiplication laws specify what happens when the class of the argument is known: \[\begin{aligned} i_1 \mathbin{\cdot_{\scriptscriptstyle S}}i_2 &= i_2 \mathbin{\cdot_{\scriptscriptstyle SS}}i_1 \text{, ignoring the possibility of overflow}\\ I_1 \mathbin{\cdot_{\scriptscriptstyle S}}i_2 &= I_1 \mathbin{\cdot}P(i_2) \\ i_1 \mathbin{\cdot_{\scriptscriptstyle L}}I_2 &= P(i_1) \mathbin{\cdot}I_2 \\ I_1 \mathbin{\cdot_{\scriptscriptstyle L}}I_2 &= I_1 \mathbin{\cdot_{\scriptscriptstyle LL}}I_2 \text{, as implemented in exercise 38} \end{aligned}\]Notice the recursion: \(\mathbin{\cdot}\) can call \(\mathbin{\cdot_{\scriptscriptstyle S}}\) which can call \(\mathbin{\cdot}\). To prevent infinite recursion, something has to get smaller at each recursive call:
One thing that might get smaller is the number of small-integer operands to the multiplication.
The other thing that might get smaller is the number of arguments whose sizes are unknown. A specific method like
multiplyByLargeNegativeInteger:
(\(\mathbin{\cdot_{\scriptscriptstyle L}}\)) has no unknown arguments, where*
(\(\mathbin{\cdot}\)) has one, somultiplyByLargeNegativeInteger
is smaller.
At each recursive call, either the number of small integers gets smaller, or the number of unknown arguments gets smaller while the number of small integers stays the same.
Using these laws and message
addSelector:withMethod:
, add methodsmultiplyBySmallInteger:
,multiplyByLargePositiveInteger:
,multiplyByLargeNegativeInteger:
, and*
to classSmallInteger
, and add methodmultiplyBySmallInteger:
to classLargeInteger
. Be aware of which methods send messages and which methods invoke a primitive. Don’t edit any existing code. Add everything by sendingaddSelector:withMethod:
messages to the existing classes, inmixnum.smt
.Test mixed multiplication. As with large-integer multiplication and addition, you need to test every method. Using the
messageTrace
technique described in the homework handout, continue to confirm that dispatch is working the way you expected. For example, given({((LargeInteger fromSmall: 4) * 5)} messageTrace)
The trace will confirm that it sends message
*
to an instance of classLargePositiveInteger
and messagemultiplyByLargePositiveInteger
to an instance of classSmallInteger
.Test all five mixed-multiplication methods using
check-print
. Add your unit tests to filemixnum.smt
.
Addition
Implement mixed addition. Addition uses the exact same promotion laws as multiplication, just with different operations. Using message
addSelector:withMethod:
, add methodsaddSmallIntegerTo:
,addLargePositiveIntegerTo:
,addLargeNegativeIntegerTo:
, and+
to classSmallInteger
, and add methodaddSmallIntegerTo:
to classLargeInteger
. As before, be aware of which methods send messages and which methods invoke a primitive.Put your added methods in file
mixnum.smt
.Test mixed addition. Repeat the process described for multiplication, testing each added method one at a time. Confirm the messages you are sending by using
messageTrace
. Add unit tests to filemixnum.smt
.
Overflow
Implement multiplication with overflow. You will now alter the multiplication of small integers so it can deal with overflow. To make this possible, you will have to use a different primitive. Instead of the
*
primitive, you will use themulWithOverflow
primitive described on page 732. You will find a short example there as well. You will useaddSelector:withMethod:
to overwrite one of the methods you have already defined with this new compiled method:(compiled-method (\(i\))
((primitive mulWithOverflow self \(i\) {\((P(\mathtt{self})\mathbin{\cdot}{}i)\)}) value)))This code works as follows:
The primitive returns a block.
If the multiplication doesn’t overflow, the block holds the product.
If the multiplication does overflow, the block is the argument block.
No matter what block comes back from the primitive,
value
is sent to it.
If multiplication doesn’t overflow, the method returns the product. If multiplication does overflow, the receiver gets promoted (making the total number of small integers smaller) and the whole cycle starts again.
Add code to your
mixnum.smt
file to replace methodmultiplyBySmallInteger:
on classSmallInteger
.Test multiplication with overflow. Add unit tests to file
mixnum.smt
.Implement addition with overflow. Using the same model as you used for multiplication, use
addSelector:withMethod:
to replace the method that adds two small integers (methodaddSmallIntegerTo:
on classSmallInteger
). Instead of the+
primitive, useaddWithOverflow
. In case of overflow, your new method should promoteself
to a large integer.Add code to
mixnum.smt
.Test addition with overflow. Add unit tests to file
mixnum.smt
.Implement subtraction and negation with overflow. Like addition and multiplication, subtraction can overflow. On most numbers, subtraction is implemented by a combination of addition and negation, using the law \[I_1 - I_2 = I_1 + (-I_2)\text.\] Unfortunately doesn’t provide a negation primitive, so in small integers, subtraction is primitive, and negation is implemented using the law \[-i = 0 - i\text.\] Both methods
-
andnegated
have to be reimplemented.You might think that either method could be primitive and implemented in terms of the other. But there is a treacherous loop here: promotion \(P(i)\) uses
negated
. The only safe way to proceed is as follows:- A small integer \(n\) can be negated safely by sending
subWithOverflow
to \(0\) with argument \(n\). If the subtraction overflows, the overflow block should answer ((\(n\) asLargeInteger) negated)
Using
addSelector:withMethod:
, replace the existingnegated
method on classSmallInteger
with a new compiled method that implements this idea.Now replace
-
onSmallInteger
with the version of-
originally defined on classNumber
:(compiled-method (y) (self + (y negated))))
Put all code in file
mixnum.smt
.- A small integer \(n\) can be negated safely by sending
Finishing up
- Confirm formatting of unit tests. Scan
mixnum.smt
and make sure all unit tests are indented 8 spaces.