Hero image for Making Sense of Code: What Semantic Analysis Actually Checks All articles
Compiler DesignComputer ScienceProgramming

Making Sense of Code: What Semantic Analysis Actually Checks

Peter K Joseph

6 min read 1,185 words
Interactive

Simulator available

Compiler Phases Simulator

Step through every compilation stage — tokenisation, parsing, IR, optimisation and code generation — one instruction at a time.

Try it

This article is Part 4 of the Compiler Design series. Part 1 · Part 2 — Lexical Analysis · Part 3 — Syntax Analysis

Consider this C code:

int x = "hello" + 3.14;

It is syntactically valid — it matches every grammar rule. The parser produces a perfectly well-formed AST. But it is nonsense: you cannot add a string literal to a floating-point number and store the result in an integer. No grammar rule catches this; it takes a different kind of analysis entirely.

That analysis is semantic analysis — Stage 3 of compilation.

What Semantic Analysis Does

The semantic analyser receives the AST from the parser and walks through it, enforcing the language’s meaning rules rather than its structural rules. These include:

  • Type checking — are operands compatible? Is an int being assigned where a float is expected?
  • Scope resolution — is every name actually declared? Is it accessible from where it’s used?
  • Function call validation — are the right number and types of arguments being passed?
  • Control flow rules — is a return statement inside a function? Is a break inside a loop?
  • Definite assignment — is a variable used before it’s initialised?

The output is an annotated AST — the same tree structure, but with type information attached to every node. This enriched tree is what the IR generator consumes next.


The Symbol Table

The workhorse of semantic analysis is the symbol table — a data structure that records every named entity in the programme: variables, functions, types, constants.

Symbol table showing entries with columns for name, type, scope, value or memory address, line number, and whether the variable has been initialised

For each identifier, the symbol table stores:

  • Name — the identifier string (x, add, MAX_SIZE)
  • Type — the declared type (int, float, fn(int, int) → int)
  • Scope — where the name is visible (global, local:main, local:add)
  • Address — the memory location or parameter position
  • Line — where it was declared (for error messages)
  • Initialised? — whether it has a value yet

The symbol table is built during semantic analysis but persists through every subsequent stage. Code generation uses it to determine where each variable lives in memory.

What Happens Without a Declaration

When the semantic analyser encounters a name that doesn’t appear in the symbol table, it reports an undeclared identifier error. This is one of the most common compiler errors and one of the most useful: the programme literally cannot proceed because the compiler has no idea what type the name has or where it lives in memory.

int result = sum(a, b);  // error: 'sum' undeclared

Type Checking

Type checking is semantic analysis’s most visible job. The analyser walks each node in the AST and verifies that the types of sub-expressions are compatible with the operation being applied.

Static vs Dynamic Type Checking

Static type checking (C, Java, Rust, TypeScript) happens at compile time. Every expression has a type that can be determined before the programme runs. The compiler rejects type-inconsistent programmes outright.

Dynamic type checking (Python, JavaScript, Ruby) happens at runtime. Types are attached to values, not variables, and checked only when an operation is actually performed.

Compiled languages are overwhelmingly statically typed because static checking moves entire categories of bugs from runtime crashes to compile-time errors — where they’re far cheaper to fix.

Type Coercion and Promotion

Some type mismatches are automatically resolved. When you add an int and a float in C, the int is promoted to a float before the operation — the semantic analyser inserts an implicit conversion node into the AST. This is distinct from an error; it’s a defined language rule.

int a = 5;
float b = 2.5;
float c = a + b;  // a promoted to float; valid

An actual type error is something the language defines as illegal:

int* p = "hello";  // pointer-to-int cannot hold string literal

Scope Resolution

A scope is a region of the programme where a name is visible. Most languages use lexical scoping (also called static scoping): the scope of a name is determined by where it’s declared in the source text, not by the call stack at runtime.

Nested scope diagram showing global scope containing function scope containing block scope, with arrows illustrating how the compiler looks up names from innermost to outermost scope

When the semantic analyser encounters a name, it searches scopes from innermost to outermost:

  1. The current block (local variables)
  2. The enclosing function (parameters and locals of outer blocks)
  3. The global scope (global variables and function declarations)

If the name is found, its declaration is used. If not, it’s an undeclared-identifier error. If a name in an inner scope matches one in an outer scope, the inner name shadows the outer one — the outer declaration is temporarily invisible.

Why Scope Rules Matter

Scope rules prevent:

  • One function’s local variables from interfering with another’s
  • Accidentally using a variable before it enters scope
  • Name collisions across large codebases (different modules can reuse names freely)

Common Semantic Errors

Error typeExampleWhat’s wrong
Undeclared identifierx = z + 1;z never declared
Type mismatchint x = "hi";string cannot be stored in int
Wrong argument countadd(1, 2, 3)add takes two parameters
Wrong argument typesqrt("hello")sqrt expects a numeric argument
Use before initialisationint x; y = x + 1;x has no value yet
Return type mismatchint f() { return "ok"; }function declared to return int
Break outside loopbreak; at top levelbreak only valid inside loops

Each of these is syntactically valid — the parser accepts them all. Only semantic analysis catches them.


The Annotated AST

The output of semantic analysis is an AST where every node carries type information. For x = 5 + y (where x is int and y is float):

Assign (=)  :int
├── x       :int
└── Add (+) :float  ← implicit promotion: result is float
    ├── 5   :int    ← widened to float
    └── y   :float

The type annotations tell the IR generator exactly what kind of operation to emit at each step — integer addition, floating-point addition, or a conversion between the two. Without this information, the code generator would have no idea which machine instruction to use.


What Comes Next

With a type-checked, annotated AST, the compiler moves to Intermediate Code Generation — Stage 4. The front end’s job is complete: the source code has been tokenised, parsed, and validated for both structure and meaning. The back end now takes over, starting by converting the AST into a machine-independent intermediate representation that the optimiser can work on.