Programming languages and translators
Machine code, assembly and imperative high-level languages, the Little Man Computer, and choosing an assembler, compiler, interpreter or bytecode.
Do this lesson in the simulatorAt GCSE (F6.5) you compared high-level and low-level languages, and compilers with interpreters, and wrote an interpreter for a made-up robot assembly language. At A level you need the precise classification of languages, real assembly languages (OCR's Little Man Computer and AQA's instruction set), how an assembler works inside, and where bytecode fits among the translators.
Classifying languages
| Class | Language | What it is |
|---|---|---|
| Low level | Machine code | binary patterns the processor executes directly: an opcode (which operation) and an operand (what to use it on) |
| Low level | Assembly language | the same instructions written as mnemonics such as ADD and STA, with names for memory locations; each line translates to one machine code instruction |
| High level | Imperative high-level language | statements that say, step by step, how to change the program's state: Python, C, Java. One statement usually becomes many machine code instructions |
Both low-level languages are processor-specific: machine code for BugBot's ESP32 means nothing to the Intel processor in a laptop. A high-level program can be translated for any processor that has a translator.
The term imperative matters. An imperative program is a sequence of commands that change variables (the state). That is the same model as machine code, which is a sequence of instructions that change registers and memory, and it is why imperative languages translate so directly into low-level code. Other paradigms, such as functional programming, describe what to compute rather than the steps.
| Machine code and assembly | High-level languages | |
|---|---|---|
| Speed and size | can be hand-tuned to be as fast and small as possible | usually larger and slower after translation, though compilers optimise well |
| Hardware access | direct control of registers, memory addresses and devices | usually through the operating system or a library |
| Writing and reading | slow to write, hard to read, easy to make mistakes in | quick to write, readable, with structures such as loops, functions and objects |
| Portability | tied to one processor family | runs on any machine with a translator |
| Used for | device drivers, bootloaders, time-critical parts of embedded systems | almost everything else |
Assembly language in the exam
OCR uses the Little Man Computer (LMC): one accumulator, 100 memory locations (mailboxes 00 to 99), and instructions written as three-digit numbers. The first digit is the opcode and the last two are an address.
| Mnemonic | Code | Effect |
|---|---|---|
ADD |
1xx | add the value in address xx to the accumulator |
SUB |
2xx | subtract the value in address xx from the accumulator |
STA |
3xx | store the accumulator in address xx |
LDA |
5xx | load the value in address xx into the accumulator |
BRA |
6xx | branch always to address xx |
BRZ |
7xx | branch to xx if the accumulator is zero |
BRP |
8xx | branch to xx if the accumulator is zero or positive |
INP |
901 | input a value into the accumulator |
OUT |
902 | output the accumulator |
HLT |
000 | stop |
DAT |
not an instruction: reserves a memory location, holding the value given (0 if none) |
This LMC program inputs two numbers and outputs their sum. first is a label: a name for an address, which the assembler replaces with the real address.
| Address | Assembly | Machine code |
|---|---|---|
| 0 | INP |
901 |
| 1 | STA first |
306 |
| 2 | INP |
901 |
| 3 | ADD first |
106 |
| 4 | OUT |
902 |
| 5 | HLT |
000 |
| 6 | first DAT |
000 |
AQA uses a register-based instruction set instead. The same idea, adding two values from memory and storing the result, looks like this:
LDR R0, 100 ; load the value in memory address 100 into register R0
LDR R1, 101 ; load the value in address 101 into R1
ADD R2, R0, R1 ; R2 = R0 + R1
STR R2, 102 ; store R2 in address 102
HALT
Here is a Little Man Computer written in Python, running the machine code from the table:
memory = [901, 306, 901, 106, 902, 0, 0] + [0] * 93
inputs = [12, 30]
acc, pc = 0, 0
while True:
code = memory[pc]
pc = pc + 1
op, address = code // 100, code % 100
if code == 0:
break
elif code == 901:
acc = inputs.pop(0)
elif code == 902:
print("OUT", acc)
elif op == 1:
acc = acc + memory[address]
elif op == 2:
acc = acc - memory[address]
elif op == 3:
memory[address] = acc
elif op == 5:
acc = memory[address]
elif op == 6:
pc = address
elif op == 7 and acc == 0:
pc = address
elif op == 8 and acc >= 0:
pc = address
Translators
A program must be translated into machine code before the processor can run it. The code the programmer writes is the source code; the translated machine code is the object code (or executable code).
- An assembler translates assembly language into machine code. Because each line is one instruction, the job is mostly looking things up: each mnemonic becomes its opcode, and each label becomes an address. A label can be used before it is defined (
BRZ endcomes before the line labelledend), so most assemblers make two passes: the first builds a symbol table of labels and their addresses, and the second produces the machine code using it. - A compiler translates the whole of a high-level program into object code before it runs. The object code runs fast and on its own, with no compiler, and the source code does not need to be given to users. But compiling takes time after every change, errors are reported only after the whole program has been analysed, and the object code runs only on the processor (and operating system) it was compiled for.
- An interpreter translates and runs a high-level program one statement at a time, every time it runs. Changes can be tested immediately and it stops at the exact line of an error, which suits development and teaching. But it runs slower, because statements inside a loop are translated again every time round, and every user needs the interpreter and the source code.
- Bytecode is the path in between. A compiler translates the source into intermediate code for a virtual machine (A10.6), and the virtual machine interprets it, or compiles it to machine code as it runs. Translation to bytecode is done once, the bytecode is portable to any machine with the virtual machine, and it runs faster than interpreting the source directly. Java and Python both work like this.
| Situation | Best choice | Why |
|---|---|---|
| Selling a game to the public | compiler | fast, standalone, and the source code stays private |
| Students learning and testing code | interpreter | immediate feedback, and it stops at the line with the error |
| An app that must run on many kinds of device | bytecode | compile once, run on any machine with the virtual machine |
| A bootloader that must fit in a few kilobytes | assembler | total control of size and hardware |
Task: an LMC assembler
Write a two-pass assembler for the LMC program in source, a list of strings, one line per address starting at address 0. Each line is one of: a mnemonic alone ("OUT"); a mnemonic and an operand ("BRZ end"); a label and a mnemonic ("end HLT"); or a label, a mnemonic and an operand ("one DAT 1"). A word is a label if it is not a key in OPCODES. An operand is either a label or a whole number.
- Pass 1: for each line that has a label, store the label and its address in a dictionary, and print
<label> = <address>. - Pass 2: for each line, the machine code is the opcode from
OPCODESplus the operand's value (the label's address, or the number, or 0 if there is no operand). Print<address>: <code>, with the code as three digits, so 0 prints as000and 1 as001.
The robot stays still.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
OPCODES = {"ADD": 100, "SUB": 200, "STA": 300, "LDA": 500, "BRA": 600, "BRZ": 700,
"BRP": 800, "INP": 901, "OUT": 902, "HLT": 0, "DAT": 0}
source = [
"INP",
"STA count",
"loop LDA count",
"OUT",
"BRZ end",
"SUB one",
"STA count",
"BRA loop",
"end HLT",
"count DAT",
"one DAT 1",
]
Challenges
- What does the
sourceprogram do? Put your machine code into the Little Man Computer above, with an input of 3, and check. - Make your assembler report an error, instead of crashing, for an unknown mnemonic or a label that is never defined.
- Write an LMC program that inputs two numbers and outputs the larger, using
SUBandBRP, and assemble it.