Semantic Analysis

A grammatically valid program can still be nonsensical — using a variable that was never declared, adding a number to a function, calling something that isn't callable. Semantic analysis is where a compiler checks that a syntactically correct program actually means something valid.

Semantic analysis checks the meaning-level rules of a program that pure grammar can't capture — is this variable actually declared before use, does this function call pass the right number and type of arguments, is this operation even valid for these types of values. It happens after parsing succeeds, on the AST the parser produced.

A program can be perfectly grammatical (parse successfully) while still being invalid — "x + true" is a well-formed expression grammatically, but adding a number to a boolean may not make sense in a given language. Semantic analysis is what catches this whole category of error before it ever becomes a runtime surprise (in statically checked languages).

Every statically typed language's compiler performs extensive semantic analysis (type checking, scope checking) before generating any code. Dynamically typed languages defer most of this checking to runtime instead, catching many of the same categories of error, just much later — the timing difference discussed in the earlier Compiled vs. Interpreted fundamentals topic.

Consider the sentence "Colorless green ideas sleep furiously." It's grammatically perfect English — subject, adjectives, verb, adverb, all correctly ordered — but it doesn't actually mean anything sensible. A grammar checker would approve it; a meaning checker would flag it. Semantic analysis is a compiler's meaning checker, run only after the grammar checker (the parser) has already approved the structure.

This code is perfectly grammatical — a valid variable declaration, initialized with a valid-looking expression, followed by a valid return statement. The parser accepts it without complaint. Only semantic analysis, which checks whether count actually refers to something declared somewhere, catches the real problem: this identifier doesn't exist.

A symbol table is the core data structure semantic analysis relies on — a mapping from each identifier (variable, function, type name) to information about it (its type, where it was declared, its scope) built up as the analyzer walks the AST, and consulted whenever that identifier is referenced elsewhere.

Scope checking verifies that every identifier used actually has a matching, visible declaration according to the language's scoping rules (covered in the earlier Scope & Lifetime fundamentals topic) — using a name that's declared in a completely different, non-visible scope is a semantic error, not a syntax error.

Type checking verifies that operations are only applied to compatible types, that function calls supply the right number and types of arguments, and that a function's return value matches its declared return type — the depth and strictness of this checking varies enormously between languages, discussed in the earlier Type Systems fundamentals topic.

Type inference (when supported) is often performed as part of semantic analysis too — deducing a variable's or expression's type from context rather than requiring an explicit annotation, while still ultimately fixing and checking that type statically.

Assuming a program that "parses fine" is therefore correct — parsing only confirms grammatical validity; a huge range of real bugs (undeclared variables, type mismatches, calling something with the wrong number of arguments) are only caught in this later stage, or not until runtime in a language that skips or defers this checking.

Confusing a semantic error's message with a syntax error's — "undeclared identifier" and "type mismatch" are semantic errors, meaningfully different from "unexpected token" or "expected ';'", which are syntax errors from the parser.

Underestimating how much of a statically typed compiler's real work happens in semantic analysis — for many real-world programs, this stage is where the vast majority of compile-time errors are actually caught, well past the point where the code merely parses.

Reading a compiler's specific error category (a type error vs. an undeclared-identifier error vs. a syntax error) to immediately narrow down what class of mistake was actually made, rather than treating every red compiler message the same way.

Using a statically typed language's type checker deliberately as a design tool — structuring types so that entire categories of invalid program states become genuine semantic errors the compiler catches, rather than silent bugs discovered later.

Understanding that in a dynamically typed language, the equivalent checks (is this operation valid for these actual runtime values) still happen — just at runtime, on the specific code paths that actually execute, rather than exhaustively at compile time.

Symbol tables as scoped, nested structures A real symbol table isn't one flat lookup — it's typically a chain or tree of scopes (global, then each function, then each nested block), and looking up an identifier means searching the current scope and then successively outward through enclosing scopes until either a match is found or the search reaches the outermost scope and fails — directly implementing a language's lexical scoping rules, discussed in the earlier Scope & Lifetime topic.

Type inference algorithms Beyond simple local inference (deducing an expression's type from its immediate subexpressions), some languages use more powerful global inference algorithms, most famously Hindley-Milner (used in ML-family languages like Haskell and OCaml), which can infer correct, maximally general types for entire functions with no annotations at all, by building and solving a system of type constraints gathered while walking the whole program.

Semantic analysis often annotates the AST rather than replacing it

Rather than producing an entirely new data structure, semantic analysis commonly decorates the existing AST in place — attaching a resolved type to every expression node, a resolved declaration to every identifier reference, implicit type-conversion nodes where a language allows automatic coercion — so that the next stage (generating an intermediate representation) can work from an AST that already carries all the meaning-level information it needs, not just raw syntax.

Give an example of a program that parses successfully but fails semantic analysis.

int main() { return undeclaredVariable; } — this is grammatically a perfectly valid function definition and return statement, so it parses without error. Semantic analysis is what catches that undeclaredVariable has no matching declaration anywhere in scope.

What is a symbol table, and what is it used for?

A data structure mapping each identifier to information about it (type, declaration location, scope) — built while walking the AST during semantic analysis, and consulted to resolve every subsequent reference to that identifier and verify it's being used correctly.

What's the difference between a syntax error, a semantic error, and a runtime error?

A syntax error means the code doesn't match the grammar at all (caught by the parser). A semantic error means the code is grammatically valid but violates a meaning-level rule, like type or scope rules (caught by semantic analysis, at compile time in statically checked languages). A runtime error occurs while the program is actually executing, and is the only category dynamically typed languages typically rely on for equivalent checks.

int main() {
    int total = count + 1;   // 'count' was never declared
    return total;
}