The problem: The Robot With One Pocket
The school shop has a new robot that stacks tins of beans into a triangle: one tin on the top row, two tins on the row below, three below that, and so on, down to N tins on the bottom row.
The robot is not clever. It has exactly one pocket, which holds one number at a time, and a row of boxes with names written on them. It follows a list of instruction cards, one card at a time, from the top:
N DC 4
SUM DC 0
LOOP LOAD SUM
ADD N
STORE SUM
LOAD N
SUB =1
STORE N
BG LOOP
PRINT SUM
END
The first two cards write 4 in the box N and 0 in the box SUM.
LOAD SUM puts a copy of what is in SUM into the pocket; ADD N adds what is in N to the pocket; STORE SUM copies the pocket back into SUM; SUB =1 takes 1 away from the pocket; BG LOOP jumps back to the card labelled LOOP if the pocket holds a number greater than 0; PRINT SUM prints the box SUM; and END stops the robot.
Two questions:
- What number does the robot print?
- If the first card said
N DC 100instead, what would it print, and what is the robot working out for anyN?
This little language is ACSL's assembly language, and the pocket is called the accumulator. It is on the ACSL Intermediate and Senior papers. Lesson 1 teaches the accumulator, memory and arithmetic, Lesson 2 teaches jumps, loops, input and output, and Lesson 3 teaches how to see what a whole program computes. Along the way you will turn assembly programs into Python, and at the end of Lesson 3 you will write a Python program that runs assembly programs, and use it to check the robot.
You will learn
- Read a line of ACSL assembly as
LABEL OPCODE LOC. - Trace the accumulator and memory through
DC,LOAD,STORE,ADD,SUB,MULT,DIVandEND, including immediate data such as=5. - Follow jumps (
BE,BG,BL,BU), loops,READandPRINT. - Work out what a program computes in general, and run assembly programs in Python.
Lesson 1 — The accumulator and memory
Division: Intermediate and above.
A computer's processor understands only very simple instructions: "fetch this number", "add that number", "store the result here". Assembly language writes those instructions as short words, one per line. Real processors each have their own assembly language, so ACSL made up a small one of its own, with the ideas they all share.
An ACSL assembly program works with two things:
- the accumulator,
ACC: one special place that holds one number, like the robot's pocket. Every calculation happens inACC, andACCstarts at 0. - memory: boxes that each hold one number, called locations.
Each location has a name, its label, such as
SUM,AorTEMP.
The program runs from the first line downwards, one line at a time, until it reaches END.
The shape of a line
Every line has up to three parts, called fields:
LABEL OPCODE LOC
LABELis optional. It names the line (and the location aDCline makes). A label starts with a letter and may go on with letters and digits. Labels are case-sensitive:SumandSUMare different labels.OPCODEsays what to do. Opcodes are always in capitals, and an opcode can never be used as a label.LOCsays which location the instruction uses.
Programs are printed in columns, with the labels at the left and a blank where a line has no label:
SUM DC 0
LOAD SUM
So you read a line like this: if its first word is an opcode, the line has no label; otherwise the first word is the label and the opcode comes next.
DONE END is an END line with the label DONE; LOAD B is a LOAD line with no label.
The first opcodes
| Opcode | What it does |
|---|---|
DC |
"define constant": the label names a new location, holding the number in LOC. ACC does not change. A DC line must have a label. |
LOAD |
copy the number in LOC into ACC. LOC does not change. |
STORE |
copy ACC into LOC. ACC does not change. |
ADD |
ACC becomes ACC + LOC |
SUB |
ACC becomes ACC − LOC |
MULT |
ACC becomes ACC × LOC |
DIV |
ACC becomes ACC ÷ LOC, keeping only the whole-number part (see below) |
END |
stop the program. Its LOC is empty. |
The arithmetic opcodes change only ACC; the location they read stays the same.
Only STORE writes into memory.
In this book, every DC line sets its location before the program starts, wherever the line sits in the program; when the robot reaches a DC line it simply moves on.
The programs here put their DC lines at the top.
Immediate data
Instead of a label, LOC can be a number written after =.
This is called immediate data: the number itself is used, not a location.
LOAD =123puts 123 inACC.SUB =1takes 1 away fromACC.MULT =-3multipliesACCby −3.
Only LOAD, ADD, SUB, MULT and DIV may use immediate data, because they only read their LOC.
STORE writes into its LOC, so STORE =5 makes no sense and is not allowed.
DIV rounds toward zero
ACSL says DIV keeps "the signed integer part of the quotient".
That means: divide, then throw away everything after the decimal point, and keep the sign.
7 DIV 2: 7 ÷ 2 = 3.5, so the answer is3.-7 DIV 2: −7 ÷ 2 = −3.5, so the answer is-3.-7 DIV -2: 3.5, so3; and7 DIV -2is-3.
For a negative answer this is not the same as the int of ACSL's pseudocode or Python's //, which both round down (−3.5 becomes −4).
DIV rounds toward zero: −3.5 becomes −3.
This is the one place in ACSL where that happens, and it is the most common trap in this category.
Also watch the order.
ACSL writes that LOC is "divided into" ACC, which means ACC ÷ LOC: ACC is the number being divided.
How big can a number be?
ACSL says ADD, SUB and MULT work modulo 1,000,000, and READ (Lesson 2) reads a number modulo 1,000,000.
For a positive number that simply means keeping the last six digits: 999999 + 1 gives 0, and 1000 MULT =1000 gives 0.
ACSL gives no example for negative numbers.
Book convention: this book keeps the sign and the last six digits, so -999999 - 2 gives -1.
This is the book's own reading of the rule, not ACSL's.
It never matters in this unit: in every exercise, every number stays between −999,999 and 999,999, and no program ever divides by zero.
Tracing a program
To trace a program, go down it line by line and write down ACC every time it changes.
Here is the first sample program from ACSL's own page:
TEMP DC 0
A DC 8
B DC -2
C DC 3
LOAD B
MULT C
ADD A
DIV B
SUB A
STORE TEMP
END
The DC lines set TEMP = 0, A = 8, B = −2 and C = 3.
| Line | ACC |
Why |
|---|---|---|
LOAD B |
−2 | copy B |
MULT C |
−6 | −2 × 3 |
ADD A |
2 | −6 + 8 |
DIV B |
−1 | 2 ÷ −2 |
SUB A |
−9 | −1 − 8 |
STORE TEMP |
−9 | ACC is copied into TEMP; ACC does not change |
So after the program runs, TEMP holds −9.
Example 2. What is in X and Y after this program?
X DC 5
Y DC 0
LOAD =20
SUB X
STORE Y
MULT =-2
DIV =4
STORE X
END
| Line | ACC |
Memory |
|---|---|---|
LOAD =20 |
20 | |
SUB X |
15 | |
STORE Y |
15 | Y = 15 |
MULT =-2 |
−30 | |
DIV =4 |
−7 | (−30 ÷ 4 = −7.5, toward zero) |
STORE X |
−7 | X = −7 |
So X holds −7 and Y holds 15.
Notice that SUB X read the old X (5): a location only changes when a STORE writes into it.
Assembly in Python
Turning a short assembly program into Python is easy: each location becomes a variable, and ACC becomes a variable too.
The only hard part is DIV.
Python's // rounds down, so we need our own function that rounds toward zero:
- divide the sizes of the two numbers with
//(for positive numbers, rounding down is rounding toward zero) - if exactly one of the two numbers is negative, the answer is negative
print(-7 // 2)
def div(x, y):
q = abs(x) // abs(y)
if x * y < 0:
q = -q
return q
print(div(-7, 2))
print(div(7, -2))
print(div(-7, -2))
print(div(7, 2))
print(div(2, -2))
Output
-4
-3
-3
3
3
-1Notice
x * y < 0 is true exactly when one of x and y is negative and the other positive.
When x is 0 the answer is 0 anyway, so the sign does not matter.
Here is ACSL's sample program in Python, printing ACC after every change, so you can compare it with the trace table.
def div(x, y):
q = abs(x) // abs(y)
if x * y < 0:
q = -q
return q
temp = 0
a = 8
b = -2
c = 3
acc = b
print(acc)
acc = acc * c
print(acc)
acc = acc + a
print(acc)
acc = div(acc, b)
print(acc)
acc = acc - a
print(acc)
temp = acc
print("TEMP =", temp)
Output
-2
-6
2
-1
-9
TEMP = -9Put it together: this program reads the three starting values of A, B and C, one per line, runs ACSL's sample program with them, and prints what ends up in TEMP.
It is saved as assets/l1.py.
Try it yourself
def div(x, y):
q = abs(x) // abs(y)
if x * y < 0:
q = -q
return q
a = int(input())
b = int(input())
c = int(input())
acc = b
acc = acc * c
acc = acc + a
acc = div(acc, b)
acc = acc - a
temp = acc
print(temp)
Run it from this unit's folder:
python assets/l1.py < assets/l1/1.in
With ACSL's values (8, −2 and 3) it prints -9.
Try assets/l1/2.in, assets/l1/3.in and assets/l1/4.in, and check each answer by tracing the program by hand.
assets/l1/4.in has a negative DIV: work out whether // would have given a different answer.
Lesson 2 — Jumps, loops, input and output
Division: Intermediate and above.
So far every program ran straight from top to bottom.
Four branch opcodes let a program jump to another line.
Their LOC is the label of the line to jump to, and three of them look at ACC to decide:
| Opcode | What it does |
|---|---|
BE |
jump to the line labelled LOC if ACC = 0 ("branch if equal") |
BG |
jump if ACC > 0 ("branch if greater") |
BL |
jump if ACC < 0 ("branch if less") |
BU |
jump always ("branch unconditionally") |
If a branch does not jump, the program simply goes on to the next line.
A branch never changes ACC.
There is no "compare" opcode.
To compare two numbers X and Y, work out X − Y in ACC and branch on its sign: it is greater than 0 when X > Y, 0 when they are equal, and less than 0 when X < Y.
Two more opcodes: READ and PRINT
| Opcode | What it does |
|---|---|
READ |
read the next input number into the location LOC. ACC does not change. |
PRINT |
print the number in the location LOC. ACC does not change. |
A STORE or a READ into a label that has no DC line simply makes that location.
A question tells you the input numbers in the order the program reads them.
When a question asks what a program prints, this book writes the printed numbers in order, on one line, separated by single spaces: 13 8 3.
Example 3. A jump forward. What does this program print?
X DC 15
Y DC 22
LOAD X
SUB Y
BL YBIG
PRINT X
BU DONE
YBIG PRINT Y
DONE END
ACC becomes 15 − 22 = −7, which is less than 0, so BL YBIG jumps to the line labelled YBIG, which prints Y.
Then the program reaches DONE END and stops.
It prints 22: the program prints the larger of X and Y.
If X were bigger, BL would not jump: the program would print X, and then BU DONE would jump over the PRINT Y line.
A jump forward over some lines works like an if … else in Python.
Example 4. A jump backward makes a loop. What does this program print?
N DC 13
LOOP PRINT N
LOAD N
SUB =5
STORE N
BG LOOP
END
PRINT N prints |
ACC after SUB =5 |
N |
BG LOOP |
|---|---|---|---|
| 13 | 8 | 8 | jumps |
| 8 | 3 | 3 | jumps |
| 3 | −2 | −2 | does not jump |
So it prints 13 8 3.
Notice that the test comes at the bottom of the loop, so the body always runs at least once, and that N still gets the value −2 before the loop stops.
Example 5. Here is ACSL's second sample program. If the input is a whole number N, what is the final value of X?
READ X
LOAD X
TOP SUB =1
BE DONE
STORE A
MULT X
STORE X
LOAD A
BU TOP
DONE END
Trace it for N = 4.
READ X makes X = 4, and LOAD X puts 4 in ACC.
At TOP, ACC |
after SUB =1 |
A |
X = ACC × X |
|---|---|---|---|
| 4 | 3 | 3 | 12 |
| 3 | 2 | 2 | 24 |
| 2 | 1 | 1 | 24 |
| 1 | 0 | — | BE DONE jumps |
So X ends as 24 = 4 × 3 × 2 × 1.
In general X = N × (N − 1) × … × 1, which is written N! ("N factorial").
This is how ACSL answers the sample: X = N!.
(The program needs N ≥ 1. For N = 0, ACC goes to −1, −2, … and never reaches 0, so the program never stops.)
Loops in Python
A jump forward is an if … else.
A jump backward is a loop, and because the test is at the bottom, the loop body runs before the first test.
One way to write that in Python is a while loop controlled by a variable going, which starts as True and is set by the test at the bottom.
Here is Example 4:
n = 13
going = True
while going:
print(n)
acc = n
acc = acc - 5
n = acc
going = acc > 0
print("N ends as", n)
Output
13
8
3
N ends as -2Put it together: this program reads N and prints the final value of X in ACSL's sample program.
In that program the test (BE DONE) comes straight after SUB =1, at the top of the loop, so here an ordinary while loop fits.
It is saved as assets/l2.py.
Try it yourself
x = int(input())
acc = x
acc = acc - 1
while acc != 0:
a = acc
acc = acc * x
x = acc
acc = a
acc = acc - 1
print(x)
Run it from this unit's folder:
python assets/l2.py < assets/l2/1.in
For N = 1 it prints 1: ACC becomes 0 straight away, so the loop never runs.
Try assets/l2/2.in, assets/l2/3.in and assets/l2/4.in too.
Line by line, match each Python line to the assembly line it comes from.
Lesson 3 — What does the program compute?
Division: Intermediate and above.
Some questions ask not "what is printed?" but "what does this program do?", as in ACSL's N! sample.
The method is always the same:
- Trace the program for a few small inputs, and put the results in a table.
- Look for the pattern.
- Check the pattern on one more input, and check the edge cases (0, 1, negative numbers) too: a program often behaves differently there.
Example 6. This program reads A and then B. What does it print?
READ A
READ B
LOAD =0
STORE P
LOOP LOAD P
ADD A
STORE P
LOAD B
SUB =1
STORE B
BG LOOP
PRINT P
END
A |
B |
printed |
|---|---|---|
| 3 | 4 | 12 |
| 5 | 2 | 10 |
| 7 | 1 | 7 |
The loop adds A to P once for each pass, and B counts the passes down, so it prints A × B.
But try B = 0: the body runs once before the test, B becomes −1, and the program prints A, not 0.
So the honest answer is: "it prints A × B whenever B ≥ 1".
Contest options are often built on exactly this kind of edge case.
An assembly interpreter in Python
Division: Senior and above.
An interpreter is a program that reads another program and runs it. Here is a plan for an interpreter for ACSL assembly.
First pass: read the program.
Split each line into words.
If the first word is not an opcode, it is a label: remember in a dict where which line number it is on, and move on to the next word.
Keep the opcodes and the LOCs in two lists, ops and locs, so line k is ops[k] and locs[k].
For a DC line, also set its location in a second dict, memory, which maps each label to its number.
Second pass: run it.
Keep acc and line, the number of the line to run next.
Run the line, and move line on by one, unless a branch jumps, when line becomes where[loc].
Stop at END.
Here is the first pass on some sample lines:
OPCODES = ["LOAD", "STORE", "ADD", "SUB", "BG", "BU", "PRINT", "DC", "END"]
for text in ["LOOP LOAD SUM", " ADD =1", "DONE END", " END"]:
parts = text.split()
if parts[0] not in OPCODES:
print("label", parts[0], "then opcode", parts[1])
else:
print("no label, opcode", parts[0])
Output
label LOOP then opcode LOAD
no label, opcode ADD
label DONE then opcode END
no label, opcode ENDNotice
A LOC that starts with = is immediate data.
The function value below turns LOC into a number: the number after the =, or the number in that memory location.
Put it together: this interpreter runs programs that use DC, LOAD, STORE, ADD, SUB, BG, BU, PRINT and END, which is enough for the robot.
Its input is a line with the number of program lines n, and then the n lines of the program.
It is saved as assets/l3.py.
Try it yourself
OPCODES = ["LOAD", "STORE", "ADD", "SUB", "BG", "BU", "PRINT", "DC", "END"]
def value(loc, memory):
if loc[0] == "=":
return int(loc[1:len(loc)])
return memory[loc]
n = int(input())
ops = []
locs = []
where = {}
memory = {}
for k in range(n):
parts = input().split()
first = 0
if parts[0] not in OPCODES:
where[parts[0]] = k
first = 1
op = parts[first]
loc = ""
if first + 1 < len(parts):
loc = parts[first + 1]
if op == "DC":
memory[parts[0]] = int(loc)
ops.append(op)
locs.append(loc)
acc = 0
line = 0
while ops[line] != "END":
op = ops[line]
loc = locs[line]
line = line + 1
if op == "LOAD":
acc = value(loc, memory)
elif op == "STORE":
memory[loc] = acc
elif op == "ADD":
acc = acc + value(loc, memory)
elif op == "SUB":
acc = acc - value(loc, memory)
elif op == "BG":
if acc > 0:
line = where[loc]
elif op == "BU":
line = where[loc]
elif op == "PRINT":
print(memory[loc])
Run it from this unit's folder:
python assets/l3.py < assets/l3/1.in
assets/l3/1.in is the robot's program.
Try assets/l3/2.in (the robot with N DC 100), assets/l3/3.in and assets/l3/4.in too, and trace each program by hand to check.
The Challenge exercise at the end of this unit asks you to add the missing opcodes.
Back to the robot
Trace the robot's program with N = 4:
At LOOP, SUM |
SUM after ADD N |
N after SUB =1 |
BG LOOP |
|---|---|---|---|
| 0 | 4 | 3 | jumps |
| 4 | 7 | 2 | jumps |
| 7 | 9 | 1 | jumps |
| 9 | 10 | 0 | does not jump |
- The robot prints 10: 4 + 3 + 2 + 1 tins, the triangle with 4 tins on the bottom row.
- For any
N≥ 1, it prints 1 + 2 + … +N, the number of tins in the whole triangle. ForN= 100 that is 5050, which is whatassets/l3.pyprints forassets/l3/2.in. (There is a shortcut: 1 + 2 + … +N=N× (N+ 1) ÷ 2, and 100 × 101 ÷ 2 = 5050.)
Recap
ACCstarts at 0;LOADcopies a value into it, andSTOREcopies it back into memory.DIVrounds toward zero, so-7÷2is-3, unlike ACSL'sint, which gives-4.BE,BGandBLjump whenACCis 0, greater than 0 or less than 0;BUalways jumps.- To say what a program computes, trace small inputs in a table, find the pattern, and check the edge cases.
You can now trace an assembly program and describe what it computes, like the robot's 1 + 2 + … + N.