Programming Languages: High-level, Low-level, Compilers and Interpreters
Why Programming Languages Exist
A processor can only execute instructions written in machine code — binary patterns of 0s and 1s. Writing programs directly in machine code is extraordinarily difficult: a single instruction such as “add the value in register A to register B” becomes a long binary string, and the programmer must track every memory address manually. Programming languages were invented to give developers a more readable and manageable way to express instructions, which must then be translated into machine code before the processor can run them.
Low-Level Languages
Low-level languages are close to the hardware. They map almost directly onto the processor’s instruction set. Two types are relevant to your exam:
Machine code is the only language a processor can execute directly. It consists entirely of binary and is specific to a processor family — code written for one architecture will not run on a different one without modification.
Assembly language replaces raw binary with short, human-readable codes called mnemonics (for example, MOV to move data between locations, ADD to add two values, SUB to subtract). Each mnemonic maps directly to exactly one machine code instruction — this is called a one-to-one correspondence. Assembly language is still tightly bound to a specific processor’s instruction set. It is used when a developer needs precise control over hardware, such as in firmware or embedded systems.
Low-level languages give precise hardware control and produce fast, memory-efficient programs — because the programmer decides exactly what the processor does. The disadvantages are that they are hard to read and write, processor-specific (not portable to different hardware), and take much longer to develop in than high-level alternatives.
High-Level Languages
High-level languages use syntax that resembles natural language — words like print, if, while and return. One high-level instruction often corresponds to many machine code instructions, so much more is achieved with fewer lines of code. Python 3, the language assessed in the practical paper, is a high-level language.
High-level languages are portable: the same Python program can run on a Windows PC, a Mac and a Linux server without modification — as long as the appropriate Python interpreter is installed. This is possible because the language abstracts away the hardware details; translation to machine code is handled by a compiler or interpreter for each platform.
High-level languages are also much easier to debug. Meaningful variable names, structured control flow and comments make programs readable to any developer familiar with the language, not just the original author. This reduces the time needed to find and fix errors.
Readability: High-level uses English-like keywords; low-level uses binary or mnemonics — much harder to interpret at a glance.
Portability: High-level programs can run on different hardware (with a suitable translator); low-level programs are tied to a specific processor instruction set.
Hardware control: Low-level gives precise control over every CPU register and memory address; high-level abstracts these details away.
Development speed: High-level is far faster to write and debug; low-level requires understanding of the processor architecture and is error-prone.
Execution efficiency: Low-level programs can be more memory-efficient and faster when optimised by a skilled programmer; high-level programs rely on the translator to produce efficient machine code.
Translators: Compilers and Interpreters
Because processors only understand machine code, programs written in high-level languages must be translated. There are two main types of translator: the compiler and the interpreter.
A compiler reads the entire source code of a program and translates it all into machine code at once, before the program runs. The output is a standalone executable file. Once compiled, the program can be distributed and run on any compatible machine without the compiler being present. Errors are reported after the whole program has been analysed — the compiler lists all the problems it found, so the developer can fix them together before re-compiling.
An interpreter translates and executes the program one statement at a time. It does not produce a machine code file; instead, it translates each statement and immediately runs it. If an error is encountered, execution stops at that line — making it immediately clear where the problem is. The interpreter must be present every time the program runs, and execution is generally slower because translation happens at run time rather than beforehand.
Imagine a Python program with 200 lines. There is a bug on line 150.
With an interpreter: The program runs fine for the first 149 lines. When the interpreter reaches line 150, it stops and reports the error. The developer fixes line 150 and runs again. This makes finding the error easy — the interpreter shows exactly which line failed.
With a compiler: The compiler analyses all 200 lines before running anything. It reports all errors it finds (including line 150). The developer fixes every error, then compiles again. Once compiled successfully, the resulting executable runs quickly on any compatible machine — no Python interpreter is needed to run it.
Developers often use an interpreter during development (fast feedback, line-by-line error location) and compile for the final product (faster execution, no dependency on the interpreter).
Compiler: translates the whole program before execution, produces a standalone machine code executable, reports errors after analysing the whole program, runs faster (no run-time translation needed). Interpreter: translates one statement at a time during execution, produces no standalone executable (interpreter must always be present), stops at the first error (easier to locate), runs slower.