Semantic Analysis

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:

FolderResponsibility
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.