Three kinds of translator
A processor executes only machine code. Everything written in any other language has to be translated, and there are three tools for the job, differing in how much they translate at once.
An assembler handles assembly language, substituting one machine instruction per mnemonic. A compiler translates an entire high-level program in advance, producing an executable that runs without the compiler present. An interpreter translates and executes one statement at a time, every time the program runs.
| Compiler | Interpreter | |
|---|---|---|
| When it translates | all at once, before running | line by line, while running |
| Produces a file? | yes, an executable | no |
| Speed when running | fast — already translated | slower — translating as it goes |
| Error reporting | a list after the whole program | stops at the first error found |
| Needed to run later? | no | yes, every time |
| Source code visible? | no, only the executable is shipped | yes, the source must be present |
| Suits | finished software for distribution | development, testing and scripting |
Why developers often use both
An interpreter reports the first error immediately and lets a change be tested without waiting for a full rebuild, which suits writing and debugging. A compiler produces something fast that can be distributed without the source. So a program is commonly developed under an interpreter and compiled for release — the two are not competing choices so much as tools for different stages.
The stages of compilation
Compilation is not one action but a sequence, and each stage catches a different kind of error. Knowing which stage rejects which mistake is a standard exam question.
Lexical analysis breaks the source into tokens, strips comments and whitespace, and builds the symbol table. Syntax analysis checks those tokens against the grammar of the language — this is where a missing bracket or semicolon is caught. Semantic analysis checks meaning: using an undeclared variable, or assigning a string to an integer. Code generation produces the machine code, and optimisation improves it.
- lexical
- source into tokenscomments and spaces removed here
- syntax
- grammar checkingmissing brackets and semicolons
- semantic
- meaning checkingundeclared variables, type mismatches
- optimisation
- improving the outputremoving redundant code, reusing registers
Syntax and semantic errors are different things
if x > 5 { with a missing closing brace is a syntax error — the grammar is violated. total = "hello" + 3 may be perfectly well-formed grammatically but meaningless, which is a semantic error. Questions regularly give an example and ask which stage would catch it, so the distinction is worth being precise about.
Bytecode and the middle way
Some languages compile to an intermediate form rather than to machine code for a particular processor. Java produces bytecode, which is then executed by a virtual machine on whatever hardware is present.
This gives portability: one compiled file runs anywhere a suitable virtual machine exists, so the program does not have to be recompiled for every processor. The cost is a little speed, since the bytecode still has to be interpreted or just-in-time compiled as it runs.
Which translator, and why
- Assembler — assembly language only, one instruction per mnemonic.
- Compiler — whole program in advance; fast to run, source stays private.
- Interpreter — statement by statement; slower, but immediate feedback while developing.
- Bytecode plus a virtual machine — portable across processors, slightly slower.
- A compiler reports all errors together; an interpreter stops at the first one.
- A compiled program does not need the translator present; an interpreted one always does.
Errors, and which tool finds them when
Errors are classified by when they are discovered, and the classification matters because it determines which tool can help.
A syntax error breaks the rules of the language and is caught by the translator before the program runs at all. A run-time error is grammatically valid but fails during execution — dividing by zero, opening a file that is not there. A logic error is worst of all: the program runs, produces an answer, and the answer is wrong. No translator can detect it, because nothing is technically incorrect.
This is why testing exists as a separate discipline from compiling. A clean compile proves only that the program is well-formed, not that it does what was intended.
| Error type | When found | Found by | Example |
|---|---|---|---|
| Syntax | before running | the translator | missing bracket |
| Run-time | during execution | the program crashing | division by zero |
| Logic | possibly never | testing, by a human | using + instead of − |
A logic error cannot be found by a translator, so it has to be tracked down the way any fault is: by narrowing the possibilities systematically rather than by rereading the code hoping to spot it.
A program that compiles is not a program that works
Compiling successfully proves only that every statement is grammatically valid and type-consistent. A program that calculates an average by dividing by the wrong count compiles perfectly and is completely wrong. That gap between "accepted by the translator" and "correct" is exactly what the testing stage of the development life cycle exists to close.
What the symbol table is for
Lexical analysis produces more than a stream of tokens — it also builds the symbol table, a record of every identifier the program uses. For each name it stores the type, the scope in which it is valid, and eventually the memory address or offset assigned to it.
Every later stage depends on it. Semantic analysis consults it to check that a variable was declared before use and that the types in an assignment are compatible. Code generation consults it to find where each variable actually lives in memory, so it can emit the right address.
This is why a single undeclared variable can produce a cascade of errors: the name is absent from the symbol table, so every subsequent use of it fails the same check.
- Identifier — the name as written in the source.
- Type — integer, real, string, or a user-defined type.
- Scope — where in the program the name is valid.
- Address or offset — filled in during code generation.