Setup
In what follows, “write and implement” means that we suggest that you write the function in pseudocode first, and then implement your function in X-Toy.
We are providing starter code for this assignment here. Please download the xtoy folder and place it in a directory where you can easily access it. This folder contains the following files:
trivial_prime_finder_starter.toy: place the functions for the trivial prime finder here.sieve_of_eratosthenes_starter.toy: place the functions for the sieve of Eratosthenes here.
To avoid headaches from debugging your code line by line, we recommend the Visual X-TOY application. Use its edit mode to write your code and its debug mode to step through your program one instruction at a time.
The autograders for this assignment are currently hosted on Cogniterra.
Prime finding in X-Toy
In this assignment, you will implement two prime-finding algorithms, the trivial prime finder and the sieve of Eratosthenes, entirely in the X-Toy assembly language, and then compare their runtimes.
In years of teaching this course, I have found that students report that this is their favorite assignment because it ties the course together. I have also seen the most resistance to it, so I would say this: picture yourself finishing and loving the experience that it gave you. Keep that in mind during the journey.
Learning objectives
In completing this assignment, you will:
- see how high-level algorithmic ideas like primality testing translate into register operations, jumps, and memory manipulation;
- practice reasoning about control flow when function calls, returns, and loops must be implemented by hand;
- emulate data structures like Boolean arrays and counters using raw memory;
- contrast the efficiency of the trivial prime finder and the sieve of Eratosthenes when expressed at the machine level, and see firsthand how the sieve achieves its speedup by avoiding repeated work;
- gain confidence reading, debugging, and extending assembly programs by tracking program state across registers and memory.
Function hierarchy
The two programs that you will write are organized as follows.
Trivial prime finder
main ├─ TrivialPrimeFinder │ └─ IsPrime │ ├─ Multiply │ └─ IsDivisor └─ PrintBools
Sieve of Eratosthenes
main ├─ SieveOfEratosthenes │ └─ CrossOffMultiples └─ PrintBools
Each function lives in a fixed range of memory addresses and communicates through designated registers, which are described in the interface accompanying each exercise below.
Important notes
- Use the correct return registers. Each function has a designated link register; using the wrong one will silently corrupt control flow.
- Trivial prime finder:
maincallsTrivialPrimeFinderusingRD;TrivialPrimeFindercallsIsPrimeusingRE;IsPrimecallsMultiplyandIsDivisorusingRF; andmaincallsPrintBoolsusingRF. - Sieve of Eratosthenes:
maincallsSieveOfEratosthenesusingRE;SieveOfEratosthenescallsCrossOffMultiplesusingRF; andmaincallsPrintBoolsusingRF.
- Trivial prime finder:
- Memory layout matters. Each function may write only to the memory cells listed in its interface, and the autograders check that no other cells are modified. Across both programs, the legal data cells are:
M[00], which holdsn(written bymain);M[01], scratch space thatTrivialPrimeFinderuses to preservepacross eachIsPrimecall;M[0x80 + p]for 0 ≤p≤n, theprimeBooleansarray;M[FF], standard input and output.
R0is always zero. Writes toR0are silently discarded, so do not use it as scratch storage. It is useful as a constant 0 operand (for example,1410copiesR1intoR4viaR4 <- R1 + R0) or for unconditional jumps (C0xxmeans “ifR0== 0, go toxx”, which is always taken).- Avoid register corruption across calls. If a function needs a value to survive a subroutine call, either confirm that the subroutine’s interface lists that register as preserved, or save the value to memory before the call and reload it afterward. For example:
TrivialPrimeFinderstorespinM[01]before eachIsPrimecall becauseIsPrimeclobbersR1;SieveOfEratosthenesreloadsnfromM[00]after eachCrossOffMultiplescall becauseCrossOffMultiplesconsumesR2.
- Respect each function’s temporary registers. The temporary registers listed in a function’s interface are the complete list of registers that the function may clobber. Writing to any other register breaks the contract with its callers; even if your code works in isolation, it will fail when combined with the rest of the program.
- Loops must terminate. A function that hangs almost always has either a bad branch target or a loop counter that is not being updated.
- In Visual X-TOY, use the step button to single-step through the suspect loop. If the same instruction fires on every iteration without changing your loop variable, you have found the bug.
- Watch for a counter that gets clobbered by a subroutine call.
- Watch for a
JMP0orJMPPwhose target points back into the loop body without first running the increment instruction.
- Test each function independently, starting from the bottom of the hierarchy. For the trivial prime finder, work in the order
Multiply,IsDivisor,IsPrime,TrivialPrimeFinder,main. For the sieve, work in the orderCrossOffMultiples,SieveOfEratosthenes,main.PrintBoolsis independent and can be written at any time, as long as a validprimeBooleansarray exists in memory starting atM[80].
Note: For the first few functions that you write, the input does not come from standard input. When debugging one of these functions in Visual X-TOY, you will need to add an extra instruction at the start of your program that loads your test values into the correct input registers.
The trivial prime finder
Testing divisibility
We begin with a function that tests whether one integer divides another using repeated subtraction. If k is 0, the function immediately returns 0, since 0 divides no number. Otherwise, it repeatedly subtracts k from p until the remainder becomes 0, in which case k is a divisor of p and the function returns 1, or the remainder becomes negative, in which case k does not divide p and the function returns 0.
Code Challenge: Write an X-Toy functionIsDivisor(p, k)that determines whetherkis a divisor ofpusing repeated subtraction.
Note: The solution to this exercise is in the starter code, so feel free to copy it into the autograder. Make sure that you understand how it works, so that you call it correctly and avoid manipulating any registers or memory that it uses.
Interface
- Memory addresses: starting address
50, last allowable address5F. - Inputs:
R1=p(an integer)R2=k(the potential divisor)
- Outputs:
R3= 1 ifkis a divisor ofp, andR3= 0 otherwise
- Temporary registers (clobbered; callers must not rely on these being preserved):
R3,R4
- Preserved registers (callers can rely on these being unchanged):
R0,R1,R2,R5,R6,R7,R8,R9,RA,RB,RC,RD,RE,RF
- Memory usage:
- None (no memory reads and no memory writes)
- Return: returns to the caller via
RF(EF00).
Multiplication
Next, we need a function that computes the product a * b as the repeated addition a + a + ... + a with b summands.
Code Challenge: Write an X-Toy functionMultiply(a, b)that computes the producta * b.
Note: Check that your solution modifies only the registers listed below, so that it does not cause errors when combined with the other functions in this assignment.
Interface
- Memory addresses: starting address
30, last allowable address3F. - Inputs:
R3=a(an integer)R4=b(an integer, consumed by the function)
- Outputs:
R6=a * b
- Temporary registers (clobbered; callers must not rely on these being preserved):
R1,R4,R5,R6R4decrements to zero by the time the loop exits.R5is left holding the loop’s jump-back address (0x33), not 0 or 1, after the function returns. Callers that need a known value inR5after the call must reinitialize it themselves.
- Preserved registers (callers can rely on these being unchanged):
R0,R2,R3,R7,R8,R9,RA,RB,RC,RD,RE,RF
- Memory usage:
- None (no memory reads and no memory writes)
- Return: returns to the caller via
RF(EF00).
Testing primality
Recall that IsPrime() can be represented in pseudocode as follows.
IsPrime(p)
for every integer k between 2 and √p
if k is a divisor of p
return false
return true
Code Challenge: Write an X-Toy functionIsPrime()that determines whether a numberpstored inR1is prime.
Hints: Note thatkis at most√pprecisely whenk*kis at mostp. Also, subroutines are your friend! Without them, you will never meet the memory requirements.
Interface
- Memory addresses: starting address
60, last allowable address7F. - Inputs:
R1=p(the number being tested)
- Outputs:
R2= 1 ifpis prime, andR2= 0 otherwise
- Temporary registers (clobbered; callers must not rely on these being preserved):
R1,R2,R3,R4,R5,R6,R7,R8,R9,RFR1is the inputp, but it is consumed during execution.
- Preserved registers (callers can rely on these being unchanged):
R0,RA,RB,RC,RD,REREis this function’s return-address register;IsPrimereads it but never writes it.
- Memory usage:
- None; all scratch values are held in registers. (Because
MultiplyclobbersR5, you may need to reinitializeR5on every iteration of your loop.)
- None; all scratch values are held in registers. (Because
- Return: returns to the caller via
RE(EE00).
Note: The functionsMultiply()andIsDivisor()are provided for you behind the scenes in the autograder, with the interfaces summarized below.
Multiply()
- Memory: lines
30through3F - Inputs:
R3=a,R4=b - Output:
R6=a * b - Clobbers:
R1,R4,R5,R6(R5is left holding0x33after the call, not1) - Preserves:
R2,R3, andR7throughRF - Return address:
RF
IsDivisor()
- Memory: lines
50through5F - Inputs:
R1=p,R2=k - Output:
R3= 1 ifR2dividesR1, and 0 otherwise - Clobbers:
R3,R4 - Preserves:
R1,R2, andR5throughRF - Return address:
RF
Finding primes
Recall that TrivialPrimeFinder() can be represented in pseudocode as follows. It is now time to implement it!
TrivialPrimeFinder(n)
primeBooleans ← array of n + 1 false boolean variables
for every integer p from 2 to n
if IsPrime(p) is true
primeBooleans[p] ← true
return primeBooleans
Code Challenge: Write an X-Toy functionTrivialPrimeFinder()that reads an integernfromM[00]and, for every integerpbetween 2 andn, storespinR1and callsIsPrime()to decide whetherpis prime. It should store each result (1 for prime, 0 for not prime) in a Boolean array in memory beginning at address0x80, so thatM[0x80 + p]holds 1 ifpis prime and 0 otherwise, and then return to the caller viaRD.
Interface
- Memory addresses: starting address
18, last allowable address2F. - Inputs:
- None directly; reads
nfromM[00].
- None directly; reads
- Outputs:
- None directly; for each integer
pwith 2 ≤p≤n, stores 1 atM[0x80 + p]ifpis prime, and 0 otherwise.
- None directly; for each integer
- Temporary registers (clobbered; callers must not rely on these being preserved):
R1,R2,R4,R5,R6,R7,REREis clobbered becauseTrivialPrimeFindercallsIsPrimeusingJAL RE, 60(FE60), which overwritesREwith the return address.
- Preserved registers (callers can rely on these being unchanged):
R0,RA,RB,RC,RDRDis this function’s return-address register;TrivialPrimeFinderreads it but never writes it.
- Memory usage:
- Reads
M[00](n) andM[01](the saved copy ofp). - Writes
M[01](scratch space holdingpacross eachIsPrimecall) andM[0x80 + p]for 2 ≤p≤n. No other memory cells are modified.
- Reads
- Return: returns to the caller via
RD(ED00).
Note: The functionsMultiply(),IsDivisor(), andIsPrime()are provided for you behind the scenes in the autograder. The interfaces ofMultiply()andIsDivisor()are given above, and the interface ofIsPrime()is summarized below.
IsPrime()
- Memory: lines
60through7F - Input:
R1=p - Output:
R2= 1 ifpis prime, and 0 otherwise - Clobbers:
R1,R2,R3,R4,R5,R6,R7,R8,R9,RF - Preserves:
RD,RE(REis the return register, which is read but not written) - Return address:
RE
Printing primes
Now that we can find primes and store them in memory, we would like to print all the primes up to a given number.
Code Challenge: Write an X-Toy functionPrintBools()that scans a block of memory beginning at address0x80and prints every value ofpfor whichM[0x80 + p]is equal to 1. It should compute the starting address (0x80) inR1and the ending address (0x80 + n) inR2, loop over the addresses from the start to the end, print the correspondingpwhenever the stored value is 1, and then return to the caller viaRF.
Interface
- Memory addresses: starting address
40, last allowable address4F. - Inputs:
- None directly; reads
nfromM[00].
- None directly; reads
- Outputs:
- None directly; prints each integer
psuch thatM[0x80 + p]is 1 to standard output (M[FF]).
- None directly; prints each integer
- Temporary registers (clobbered; callers must not rely on these being preserved):
R1,R2,R4,R5,R7,R9- Because
PrintBoolsruns at the end of the program, callers do not need to preserve any of these.
- Preserved registers (callers can rely on these being unchanged):
R0,R3,R6,R8,RA,RB,RC,RD,RE,RFRFis this function’s return-address register;PrintBoolsreads it but never writes it.
- Memory usage:
- Reads
M[00](n) andM[0x80 + p]for 0 ≤p≤n. - Writes
M[FF](standard output), once per prime found. No other memory cells are modified.
- Reads
- Return: returns to the caller via
RF(EF00).
The sieve of Eratosthenes
Crossing off multiples
Recall that CrossOffMultiples(), the main subroutine of the sieve of Eratosthenes, can be represented in pseudocode as follows.
CrossOffMultiples(primeBooleans, p)
n ← length(primeBooleans) - 1
for every multiple k of p (from 2p to n)
primeBooleans[k] ← false
return primeBooleans
Code Challenge: Write an X-Toy functionCrossOffMultiples(n, p)that, given a prime numberpand a boundn, marks every multiple ofpbetween2pandn(inclusive) as composite in the Boolean array beginning at memory address0x80by setting those entries equal to 0.
Interface
- Memory addresses: starting address
50, last allowable address5F. - Inputs:
R1=p(the prime whose multiples we cross off)R2=n(the largest number we are sieving up to)
- Outputs:
- None directly; writes 0 to
M[0x80 + k]for each multiplekofpbetween2pandn.
- None directly; writes 0 to
- Temporary registers (clobbered; callers must not rely on these being preserved):
R2,R3,R4,R5,R6R2is the inputn, but it is consumed during the loop. Callers that neednafter the call must save it themselves (for example, toM[00]) before calling.
- Preserved registers (callers can rely on these being unchanged):
R0,R1,R7,R8,R9,RA,RB,RC,RD,RE,RF
- Memory usage:
- Reads: none.
- Writes
M[0x80 + k]for each multiplek=2p,3p, and so on, up to and possibly includingn. No other memory cells are modified.
- Return: returns to the caller via
RF(EF00).
Sieving
We now turn to SieveOfEratosthenes(), whose pseudocode is shown below.
SieveOfEratosthenes(n)
primeBooleans ← array of n + 1 true boolean variables
primeBooleans[0] ← false
primeBooleans[1] ← false
for every integer p between 2 and √n
if primeBooleans[p] = true
primeBooleans ← CrossOffMultiples(primeBooleans, p)
return primeBooleans
Code Challenge: Write an X-Toy functionSieveOfEratosthenes(n)that usesCrossOffMultiples()to fill a Boolean array of lengthn + 1beginning atM[0x80], whereM[0x80 + p]is 1 ifpis prime and 0 ifpis not prime.
Note: The autograder does not check that you rangeponly up to√n. You may want to add a multiplication function to help you do so, or you can implement a sieve in which theforloop ranges over every integerpbetween 2 andn.
Interface
- Memory addresses: starting address
20, last allowable address3F. - Inputs:
R2=n
- Outputs:
- None directly; for each integer
pwith 0 ≤p≤n, stores a primality flag atM[0x80 + p].
- None directly; for each integer
- Temporary registers (clobbered; callers must not rely on these being preserved):
R1,R2,R3,R4,R5,R6,R9,RFR2is the inputn, but it is consumed during the function.
- Preserved registers (callers can rely on these being unchanged):
R0,R7,R8,RA,RB,RC,RD,REREis this function’s return-address register;SieveOfEratosthenesreads it but never writes it.
- Memory usage:
- Reads
M[00](nis reloaded afterR2is consumed). - Writes
M[00](nis stored here on entry as a safety copy) andM[0x80 + p]for 0 ≤p≤n; in particular,M[80]andM[81]are set to 0. No other memory cells are modified.
- Reads
- Return: returns to the caller via
RE(EE00).
Note: The functionCrossOffMultiples()is provided for you behind the scenes in the autograder. It occupies lines50through5F, takesR1=pandR2=n(note thatnis passed inR2, not read fromM[00]), clobbersR2,R3,R4,R5,R6, preservesR1andR7throughRF, and returns viaRF.
Putting it all together
Once all your functions pass their autograders, complete the main function at address 10 of each starter file. In trivial_prime_finder_starter.toy, main should read n from standard input, store it at M[00], call TrivialPrimeFinder and then PrintBools, and halt. In sieve_of_eratosthenes_starter.toy, main should do the same with SieveOfEratosthenes in place of TrivialPrimeFinder.
Then, run both programs on increasing values of n and compare their runtimes. How much faster is the sieve at the machine level?