Semantic analysis and types
The semantic passes that resolve Prismio names, types, overloads, calls, fields, flow, and program validity.
Last verified
src/sema/checker.psm coordinates semantic analysis. Supporting modules divide builtins, types,
symbols, generics, enums, ownership, and flow so that each rule has one primary owner.
Responsibilities
Semantic analysis resolves declarations and lexical scopes, assigns expression types, selects exact overloads, validates fields and enum variants, checks calls, and establishes whether control flow returns or becomes unreachable. It also invokes ownership checks before code generation.
Prismio does not rely on broad implicit numeric promotion. Width and signedness are part of a numeric type, and casts must be explicit where the source and destination differ. Overload selection therefore uses concrete argument types rather than a general conversion ranking.
Builtins and rewrites
src/sema/builtins.psm owns operations that look call-shaped in source but lower as compiler
operations. Methods, operators, iteration, and selected standard-library behavior can be rewritten
to ordinary calls before overload resolution. The diagnostic should name the source-level action
the developer attempted, including a missing import when the rewrite depends on a standard module.
Flow facts
src/sema/flow.psm tracks returns, unreachable statements, loop behavior, and facts used by
safe lowering. A successful semantic pass is the contract presented to AIF and LLVM generation.
Backend code should not silently reinterpret an unresolved or invalid AST node.
New rules need a positive example, a focused rejection, and a multiple-error test when recovery or continued checking is expected.
Analysis order
analyzeModule is the semantic entry point. Its ordering is part of the contract:
- reset semantic type/symbol state and index top-level declarations;
- collect generic templates and predeclare concrete functions and globals;
- expand trait default methods and required generic specializations;
- validate implementation coherence, orphan rules, members, and supertrait cycles;
- synthesize dynamic trait-object structs and resolve opaque
impl Traitreturns; - analyze function bodies, globals, workloads, and expressions;
- run ownership/extern-contract checks; and
- leave a resolved
TypeInfoon every expression that may reach AIF or IR generation.
Moving body analysis before predeclaration breaks forward calls and mutual recursion. Running trait-object synthesis before conformance is known can create a vtable layout for an invalid implementation. Backend code assumes this order already succeeded.
Expression and statement functions
semaExpr(module, expr) is the central dispatcher. It handles literal types, identifier lookup,
unary/binary operators, calls, members, indexing, casts, optionals, arrays, lists, structs,
closures, dynamic values, and builtins. It returns a TypeInfo and stores that same resolved type
on the node.
semaStatement checks one statement against the enclosing function's expected return type.
semaBlock manages scope and analyzes its chain. semaFunction creates the parameter scope,
validates the body, checks return behavior, and then closes the scope.
semaPredeclareFunction and semaPredeclareGlobal establish names before bodies execute.
semaTypeErrorAt compares the expected and actual TypeInfo and formats a source-level
diagnostic. typeEquals is structural for applied built-ins/generics and nominal where the
language requires identity. typeDisplay is for humans; typeSemKey is for semantic caches;
typeIrKey is for lowering. These strings have different stability requirements and must not be
used interchangeably.
Builtins and source rewrites
semaBuiltinCallType returns the result type for compiler-known operations.
semaCheckBuiltinCall validates arity, receiver/element types, mutation, ownership, and special
constraints. The set includes list, slice, DataView, string, task, channel, optional, and selected
numeric operations.
Source sugar is rewritten before final overload resolution:
semaStringComparisonandsemaStringConcatChainroute string operators to supported operations while preserving one evaluation of each operand;semaForEachDesugarandsemaForEachIteratorturn iteration syntax into explicit iterator operations;semaPropertyRewriteconverts supported property-style access to a call; andsemaBecomeCallreplaces an expression node with the resolved call shape without discarding its source span.
When a rewrite depends on a standard module, semaStringSugarAvailable diagnoses the missing
import at the source operator rather than exposing an internal helper symbol.
Flow and program validity
semaBlock also invokes flow checks. semaStmtDiverges identifies returns, breaks, continues,
fully diverging if chains, and unbroken infinite loop statements. semaBlockDiverges walks a
block until one statement prevents fall-through. The private semaBlockHasBreak and
semaIfHasBreak helpers keep a loop with any reachable structured break from being classified
as divergent. These answers drive definite-return and unreachable-code diagnostics.
semaTaskResultAllowed and semaSpawnArgAllowed keep task ABI values within implemented
representations. semaCheckUniqueArgs prevents one uniquely consumed value from being passed
twice in a call. semaCheckExternContracts validates FFI ownership annotations only after types
and parameter positions are known.
A semantic change is complete only when it defines accepted and rejected types, coercions, source span ownership, overload interaction, flow behavior, ownership mode, generic specialization, dynamic dispatch behavior, AIF visibility, and the exact resolved form presented to codegen.