Skip to main content

Semantic Analysis & HIR — Types, Symbols, and Desugaring

STABLE(Type inference, symbol resolution, HIR lowering, scoping rules, generic bounds)

The parser gives us a syntax tree. The next stage checks whether the program means anything coherent: are names defined? do types line up? is control flow legal? This is semantic analysis, and it runs on an intermediate representation called HIR (High-Level IR).

Core invariant: semantic analysis runs on desugared HIR before any LIR lowering — the checker always sees the simplified, canonical form of your code.


Pipeline Position

Source → Lexer → Parser → AST ──► HIR ──► Semantic Analysis ──► MIR/Borrow → VIR → Backends
│ │ │
desugar resolve+scopes type check

HIR is the bridge: the AST is syntax-faithful; HIR is semantics-ready.


Stage 3 — HIR: the desugared program

Sources: src/ir/hir (mod.rs, lower.rs), src/parsing/hir_lower.rs, src/parsing/hir_passes.rs, src/parsing/hir.rs

The HIR pass takes the AST and produces a cleaned, fully-resolved intermediate tree:

Desugaring Table

Surface SyntaxHIR Canonical FormNotes
if cond { } elif cond2 { } else { }nested if with else brancheselif is sugar
cond ? a : b / ternaryif cond { a } else { b }normalized early
a ?? b (nullish coalesce)if a != null { a } else { b }desugared
a?.b (optional chaining)if a != null { a.b } else { null }desugared
...spread in arrays/callsexplicit Spread HIR nodelowered explicitly
for x in iter { }while + iterator protocoldesugared
this / self receiverssingle canonical selfunified in HIR
Decorators @pureattached metadata on HIR nodevalidated later

What HIR Resolves

  • Symbol binding — every Identifier is bound to its declaration (variable, function, class, struct, enum, import, generic param). Unbound names are an error here.
  • Scope explicitation — block scopes, function scopes, class scopes, loop scopes, and closure captures are explicit nodes. Shadowing is validated.
  • Receiver normalization — both this and self become one canonical form so later passes have a single case to handle.
  • Import resolutionimport Foo from "./bar.adesh" is resolved to a module path; missing modules are reported with source spans.

HIR Node Sketch (ASCII)

HIR Module
├── HIR Item: Function `main`
│ ├── params: [HirParam { name, ty, span }]
│ ├── body: HirBlock
│ │ ├── HirStmt::Let { name, ty?, init, scope_id }
│ │ ├── HirStmt::Expr( HirExpr::Call { callee, args } )
│ │ └── HirStmt::Return( HirExpr::Literal )
│ └── scope: ScopeId(3) ──► SymbolTable entry
├── HIR Item: Class `Counter`
│ ├── fields: [HirField { name, ty, visibility }]
│ ├── methods: [HirFunction]
│ └── extends: Option<HirType>
└── imports: [HirImport { path, alias, span }]

Symbol Table & Scope Model

Sources: src/parsing/ast.rs (scoped nodes), src/semantics/engine.rs, src/ir/hir/mod.rs

Scope Hierarchy

Global Scope (module)
├── Function Scope (fn foo)
│ ├── Block Scope (if/while body)
│ │ └── Loop Scope (for/while)
│ └── Closure Scope (captures parent bindings)
└── Class Scope (class Bar)
└── Method Scope (fn method(self))

Scopes are lexically nested and form a tree. Each scope owns a SymbolTable:

ConceptRepresentation
ScopeIdArena index into scope tree
Symbol{ name, kind, type, span, scope_id, mutable, shadowed_from }
SymbolKindVariable, Function, Class, Struct, Enum, EnumVariant, Param, Import, GenericParam, Field
ResolutionResolved(SymbolId) or Unresolved (error)

Resolution Algorithm

resolve(name, current_scope):
1. look in current_scope.table[name] → if found, return SymbolId
2. else walk parent scopes outward (lexical chain)
3. if reached global and not found → check imports
4. if still not found → emit E0001 "undefined variable `name`"
5. if found but in wrong kind (call variable as function) → emit E0002

Shadowing is allowed within nested blocks but the HIR records shadowed_from so diagnostics can note previous declaration at line X.

let x = 10;
{
let x = 20; // shadows outer x — legal, HIR tracks shadow chain
print(x); // 20 — resolves to inner scope
}
print(x); // 10 — resolves to outer scope

Closure Capture Analysis

Source: src/parsing/closure_capture.rs

Closures capture variables from enclosing scopes by reference. HIR annotates each closure with its capture set:

fn makeCounter(start: i32) {
let n = start;
return fn() { n += 1; return n; }; // captures `n`
}

HIR marks n as Captured(Mutable) — later MIR decides whether this needs an Arc<Mutex> or stack slot.


Stage 4 — The Semantic / Type Checker

Source: src/semantics/engine.rs, src/semantics/mod.rs, src/types, src/typesystem

The semantic engine performs type inference, type checking, and legality validation over the HIR.

What It Checks

CheckWhat It CatchesExample Error
Name resolutionundefined variable, function, or typeuse of undeclared variable 'foo'
Type compatibilityassignment / argument / return mismatchexpected i32, found string
Function signaturesarg count / type mismatch, missing returnfn foo(a: i32) called with 0 args
Field / property accessnon-existent field, wrong receivertype 'User' has no field 'email2'
Control flowbreak/continue outside loop, return outside fnbreak outside loop
Class / struct rulesbad extends, missing super(), visibilitysuper() must be first in derived constructor
Pattern matchingnon-exhaustive match, wrong arm typesmatch arm type mismatch
Mutabilityassign to immutable, move of borrowedcannot assign to immutable 'x'
Decorator legalityunknown / misplaced decoratorsunknown decorator @pure2
Generic boundsunsatisfied trait boundstype 'T' does not satisfy bound 'Comparable'
Varianceinvalid variance in generic positionsinvariant type parameter used covariantly

Type Inference

AdeshLang infers types without annotations where possible:

let count = 42; // inferred: i32 (numeric default)
let name = "Adesh"; // inferred: String
let items = [1, 2, 3]; // inferred: Array<i32>
let flag = true; // inferred: bool

fn add(a, b) { return a + b; } // params inferred from call sites if possible

// Annotations refine inference
let precise: u64 = 18446744073709551615;
let ratio: f64 = 10 / 3;
let typed: Array<i32> = [1, 2, 3];

Inference produces a TypeVar that is later unified. Widening rules (e.g., i32 + f64f64) are applied at operator sites.

Type System Reference

TypeSyntaxNotes
Primitivesi32, i64, u64, f32, f64, bool, string, charvalue types
BigIntbigint (42n)arbitrary precision
Complexcomplexvia cmath
ArraysArray<T> / [T]growable
Tuples[T, U]fixed arity
Objects{ key: Type }structural
Functionsfn(T): Ufirst-class
GenericsVec<T>, HashMap<K,V>with bounds
Type aliastype Pair<A,B> = [A, B]alias + generics
Union/OptionT | nullnullable
Decorators@pure, @noallocaffect checking

Generic bounds and type aliases are resolved between HIR and semantic analysis. See Type System and Type Aliases.

Resolution vs Inference — Order of Operations

HIR ──► 1. Scope resolution (bind names → SymbolId)
──► 2. Type variable creation (fresh TypeVar per unannotated binding)
──► 3. Constraint collection (unify call args, assignments, returns)
──► 4. Constraint solving (fix TypeVars, emit mismatches)
──► 5. Control-flow / decorator / class-rule validation
──► 6. Emit diagnostics (grouped, sorted by span)

Diagnostics & Error Table

CodeCategoryExample MessageSource Span
E0001Nameundefined variable 'x'identifier span
E0002Typemismatched types: expected 'i32', found 'string'expression span
E0003Signatureexpected 2 arguments, found 3call span
E0004Controlbreak outside loopkeyword span
E0005Classclass 'Foo' does not extend anything, super() is illegalsuper() span
E0006Patternnon-exhaustive match: missing variant 'None'match span
E0007Mutabilitycannot assign to immutable binding 'x'assignment span
E0008Decoratordecorator '@pure' not allowed on classdecorator span
E0009Generictype argument count mismatch: expected 2, found 1type annotation span
E0010Borrow*use of moved value 'v'use span (MIR, but surfaced here)

* Borrow errors are technically MIR-stage but surfaced through the same diagnostic pipeline (src/parsing/error.rs).

Error rendering:

--> src/main.adesh:3:8
|
3 | let x: i32 = "hello";
| --- ^^^^^^^ expected `i32`, found `string`
| |
| type annotated here

Semantics Rules (Language-Facing)

The rules checked here are documented in Semantics Rules:

  • Mutability: let vs let mut, field mutability, borrow exclusivity.
  • Shadowing: allowed in nested blocks, not in same scope.
  • Evaluation order: left-to-right for arguments, short-circuit for &&/||/??.
  • Hoisting: functions hoisted within module scope; variables not hoisted (temporal dead zone).
  • Decorator effects: @pure forbids I/O and mutation of captured state.

Worked Lowering Example

Source:

fn sum(arr: Array<i32>): i32 {
let total = 0;
for x in arr { total += x; }
return total;
}

HIR (pseudo):

HirFn sum(arr: Array<i32>) -> i32
HirBlock scope#1
HirLet total: i32 = 0 (scope#1)
HirWhile desugared from for:
HirLet x = iterator.next()
HirAssign total = total + x
HirReturn total
Symbols: arr→Param#0, total→Var#1, x→Var#2
Types: total:i32, x:i32, arr:Array<i32>

Semantic checks pass: arr has .next() (iterable), total + x unifies to i32, return matches declared -> i32.


Where This Happens in the Source Tree

ConcernSource
HIR definition & loweringsrc/ir/hir (mod.rs, lower.rs), src/parsing/hir_lower.rs, src/parsing/hir_passes.rs, src/parsing/hir.rs
Semantic engine (type inference/check)src/semantics/engine.rs, src/semantics/mod.rs
Type system & type checkingsrc/types, src/typesystem, src/typesystem/value_optimized.rs
Scope resolutionsrc/parsing/ast.rs (scoped nodes), src/ir/hir/mod.rs
Closure capturessrc/parsing/closure_capture.rs
Variance & genericssrc/parsing/variance.rs, src/parsing/type_decls.rs
Decoratorssrc/parsing/decorator_compile.rs, decorator_pipeline.rs, decorator_registry.rs
Unused / warning lintssrc/parsing/unused_warnings.rs
Error renderingsrc/parsing/error.rs

After Semantics: Safety Analysis

Once types check, the compiler moves to the ownership & borrow checker stage — see Memory Safety Analysis — then to Intermediate Representations.

The Full Context