Semantic Analysis

Semantic Analysis

The semantic analyzer's job is to turn a merely parsed program into an understood one: every expression gets a resolved type, every name gets a meaning, and anything inconsistent is rejected with an error. It never rewrites the program — it annotates the AST in place, and the borrow checker and codegen simply read those annotations.

Two Passes

It makes two passes over the merged AST:

  1. Declaration collection — a shallow sweep that registers the names of things: classes, interfaces, enums, and function signatures. This is what lets code call a function or use a type that is defined further down the file. Generic definitions (classes, interfaces, enums) are registered as templates, not concrete types. Bodies are not analyzed yet.
  2. Full analysis — a deep walk of every statement and expression. Each node is checked against its context and stamped with a resolved sun::Type.

Scope Tree

All name resolution happens against a scope tree that mirrors the program's structure: a global scope with module, import, function, and block scopes nested inside it. Looking up a name walks from the innermost scope outward, also consulting using imports and, for code that came from a .moon library, the scope of the module that defined it. Function scopes additionally carry context the analysis needs later — whether the function may throw, and its return type (used to infer types in return position).

The Deep Walk

During the deep walk, a few things are happening at once:

  • Type inference — the type of every expression is computed bottom-up (a literal is an i32, a call has its callee's return type, and so on).
  • Expected-type propagation — types also flow top-down where context determines them: a variable's annotation shapes its initializer, a function's return type shapes its return expressions, a parameter type shapes its argument. This is how integer literals adopt the right width and how Option.None learns its type arguments.
  • Checking — assignments, arguments, and operators are verified for compatibility; overloads are resolved against the actual argument types; calls to throwing functions must sit inside try or a throwing function; enum matches must be exhaustive.
  • Argument conversions — once a call's signature is settled, each argument is tagged with how it reaches its parameter (ArgConversion: pass by value, move, borrow, class to interface, numeric widening, and so on — include/semantic_analysis/argument_conversion.h). The rules live in sun::conversions::classifyArgument; codegen only carries the tags out.
  • Capture detection — lambdas are scanned for references to outer variables so codegen can build their closure environments.

Generics

Generics are monomorphized here, not in codegen. When analysis meets Vec<i32> or Option<f64>, it instantiates the template with the type arguments bound: annotations inside the template resolve against those bindings, and the resulting concrete type (plus, for classes, a cloned and re-analyzed copy of the method bodies) is recorded on the template's AST node. Codegen later walks those recorded specializations and emits each one as ordinary non-generic code.

Instantiation happens lazily at the first use, but the template's body is analyzed in the scope the template was declared in: every generic template records its definitionScope when registered, and the instantiators switch currentScope to it (ScopeSwitchGuard) before pushing the transient type-parameter / class / function scopes. So a body sees exactly the names its author could see — its module's private helpers, the module's usings, transitive dependencies — and never the requester's locals or imports. Only the resulting specialized type is registered back in the requesting scope (as a lookup fast path); identity lives in the global TypeRegistry and the specialization list on the template's AST.

Access Control

Every declaration record (FunctionInfo, ClassType, InterfaceType, EnumType, the Generic*Infos, ModuleScope, class/interface members) carries a sun::Visibility and — except members, which use their type's — a QualifiedName whose owner() (modulePath) is the module that declared it. modulePath differs from scopePath for nested items: it never contains class or function segments. The single predicate is isAccessibleFrom(from, visibility, owner) (include/semantic_analysis/visibility.h): public, or from is the owner module or one of its descendants. Contents of an imported .moon live under a $hash$ scope segment, so importer code is never inside a bundle module and bundle-private items are hidden by the same rule.

Enforcement lives in two places:

  • Module-level items are filtered inside the scope lookups (AccessFilter in semantic_scope.h, used by lookupInChain, lookupFunction, resolveNameWithUsings, findSymbolInModule, ...). The analyzer implements AccessContext, which tells lookups which module is asking and reports a denial ("helper is private to module sun") when the only candidates were private.
  • Class / interface members are resolved on the type (SemanticAnalyzer::accessibleField / accessibleMethod), which check the member's visibility (owner = the type's module) where obj.member is resolved.

Because generic bodies are analyzed inside their definition scope, "which module is asking" is always the nearest Module scope on the stack — no override is needed. deinit is always public (methodVisibility). Wording of every diagnostic comes from sun::access::denialMessage (access_checker.h).

Source Layout

The analyzer's source is split by concern under src/semantic_analysis/:

FileResponsibility
declaration_collection.cppPass one: registering declared names
analysis.cppThe main expression walk
type_inference.cppBottom-up typing
type_conversion.cppAnnotation resolution and type-parameter substitution
scope_variables.cpp / scope_lookup.cppThe scope tree
classes.cpp, interfaces.cpp, enums.cppPer-construct analysis and generic instantiation
captures.cppClosure captures