Programming languages and translators

Machine code, assembly and imperative high-level languages, the Little Man Computer, and choosing an assembler, compiler, interpreter or bytecode.

A10.7Operating systems, software and translatorsA level45 min

Do this lesson in the simulator

At 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

Run this in the simulator

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 end comes before the line labelled end), 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 OPCODES plus 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 as 000 and 1 as 001.

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

  1. What does the source program do? Put your machine code into the Little Man Computer above, with an input of 3, and check.
  2. Make your assembler report an error, instead of crashing, for an unknown mnemonic or a label that is never defined.
  3. Write an LMC program that inputs two numbers and outputs the larger, using SUB and BRP, and assemble it.