Semantic Analysis
The role of semantic analysis is essentially to assign types to every symbol and expression, and to validate semantic consistency. For example if we have a statement such as
var x = foo(bar());analysis checks that the return type of bar is compatible with the
parameter type of foo and sets the type of x to be foo's return-type.
Analysis runs a series of "passes" over the syntax tree. Each pass populates
analysis data in an AnalaysisResults object which is shared with codegen.
Some analysis data is also stored directly on the AST nodes in their "analysis" field.
Source organization
Headers in include/semantic_analysis/ and implementations in
src/semantic_analysis/ use matching topic folders:
| Folder | Responsibility |
|---|---|
body_analysis/ | Statement traversal, control flow, and function scopes |
declaration_analysis/ | Declarations, signatures, captures, and global initializer dependencies |
call_analysis/ | Call resolution, signatures, and argument conversion |
class_analysis/ | Class definitions, field initialization, and packed layout |
enum_analysis/ | Enum definitions, construction, and matching |
expression_analysis/ | Expression resolution, value properties, and type guards |
generic_analysis/ | Specialization and contextual type-argument deduction |
interface_analysis/ | Interface contracts, default methods, and class conformance |
type_analysis/ | Type resolution, inference, and compatibility |
Shared context, declaration identities, scopes, and pipeline coordination remain
at the root. Ordered passes live in passes/, and constant evaluation lives in
constants/.
SemanticAnalyzer owns the analysis session and its services. Its node entry
point establishes source context, then dispatches to DeclarationAnalyzer,
BodyAnalyzer, or ExpressionAnalyzer. The ordered registration passes remain
in the pipeline.
DeclarationAnalyzer checks declarations and signatures, discovers captures,
and tracks pending global initializers. It delegates class, interface, and enum
declarations to their specialized analyzers. ClassAnalyzer checks class
members, partial classes, and packed layout restrictions. InterfaceAnalyzer
prepares contracts on demand, checks defaults, and validates class conformance.
ExpressionAnalyzer resolves value expressions, result types, conversions,
and access to values. It cooperates with CallAnalyzer, TypeResolver, and
GenericSpecializer. BodyAnalyzer checks statements and control flow and
manages function-body scopes. Analyzers call the services that own each check;
the session facade does not forward individual checking helpers.
Ambient lifetime bindings belong to SemanticContext. A LifetimeScopeGuard
restores names and receiver-lifetime permission when a nested declaration,
body, or isolated global initializer exits, including on an error. Preparation
caches and recursion tracking remain with the analyzer performing that work.
High-Level Architecture
Imported bundles are prepared before source declarations. The bundle currently being built is treated as source.
Registration makes declarations available before their uses are checked.
ImportLibraryDeclarationsPass
registers every imported library's declaration records in DeclarationTable
before exact dependencies are validated. Records are restored with owners and
modules before their dependents, so import order does not determine which
declarations can be found.
Body checking resolves references, selects calls, infers expression types, checks type compatibility, and records captures and conversions. Global initializers are then classified as values that can be stored in the program image or work that must run at startup.
Data shared with codegen
Analysis produces two connected outputs: program-wide records in
AnalysisResults, and annotations plus specialized bodies on the AST.
A type describes the values an expression can have. The compiler represents
it with a semantic Type: an integer type describes its width and signedness;
a class type describes its fields, their types, and its method signatures.
TypeRegistry holds the descriptions of declared classes, interfaces and enums,
including concrete generic instances such as Box<i32>. Primitive types,
references, arrays and function signatures also have semantic type descriptions,
but are not entries in this registry.
A declaration introduces an entity into the program, such as a class,
function, variable or field. DeclarationTable gives each one an identity and
records its name, kind, enclosing declaration and module. Where available, the
record also links to its syntax. Two variables can have the same type while
remaining distinct declarations.
An AST annotation records what analysis learned about a particular piece of syntax. A variable reference records which declaration it refers to and its resolved type. A call records the selected function and any argument conversions. Codegen reads these decisions when emitting that expression; function and class bodies remain in the AST, including generated specializations.
Example: point.x
Suppose point has type Point, with a field x: i32. The diagrams show selected
fields using illustrative declaration IDs: #10 for Point, #11 for x, and
#1 for their module.
AST node. Syntax and analysis results live together. The analysis block records which field was selected and the type of the resulting expression.
Type registry value. Looking up the class identity gives its semantic shape. The field's type describes its values; its declaration ID identifies the field.
Declaration record. The same field ID leads to a record describing what was
declared and where it belongs. Its owner is the Point class.
A DeclarationId connects these structures within one compilation.
PortableDeclarationKey provides identity across bundle boundaries. Names
support source lookup; identities distinguish declarations even when names match.
Scopes and the specialization queue are analysis working state. Scopes determine which names are visible; the queue tracks bodies still needing checks. Codegen uses the recorded results rather than repeating that work.
Generics: signatures before bodies
Specialization prepares concrete types and signatures immediately, then queues
the bodies that need checking. For example, Box<T>.get() T becomes
Box<i32>.get() i32, so a caller knows the result type before the method's
implementation is checked.
Each queue-draining stage checks all pending bodies, including new specializations requested by those bodies. Repeated requests reuse the same specialization. Bodies resolve names in their template's definition scope, preserving its imports and visibility rules.
Recognized compiled specializations reuse their existing implementations.
Abstract shapes containing unresolved type parameters support template checking
but are not emitted as concrete implementations. Named functions without return
annotations use void; their return types are not inferred from their bodies.