Type Environment

The type environment tracks variable bindings during type checking. It uses a stack of scopes for lexical scoping.

Structure

pub struct TypeEnv {
    /// Stack of scopes (innermost last)
    scopes: Vec<Scope>,
}

pub struct Scope {
    /// Variable name -> Type
    bindings: HashMap<Name, Type>,

    /// Optional scope kind for error messages
    kind: ScopeKind,
}

pub enum ScopeKind {
    Global,
    Function(Name),
    Block,
    Lambda,
    ForLoop,
}

Operations

Creating Environment

impl TypeEnv {
    pub fn new() -> Self {
        Self {
            scopes: vec![Scope::global()],
        }
    }

    pub fn with_prelude() -> Self {
        let mut env = Self::new();

        // Add built-in functions
        env.bind_builtin("print", Type::Function {
            params: vec![Type::String],
            ret: Box::new(Type::Void),
            capabilities: vec![],
        });

        env.bind_builtin("len", Type::Function {
            params: vec![Type::TypeVar(TypeVarId(0))],  // Generic
            ret: Box::new(Type::Int),
            capabilities: vec![],
        });

        // ... more builtins

        env
    }
}

Scope Management

impl TypeEnv {
    pub fn push_scope(&mut self) {
        self.scopes.push(Scope::block());
    }

    pub fn push_function_scope(&mut self, name: Name) {
        self.scopes.push(Scope::function(name));
    }

    pub fn pop_scope(&mut self) {
        self.scopes.pop();
    }
}

Variable Binding

impl TypeEnv {
    pub fn bind(&mut self, name: Name, ty: Type) {
        let scope = self.scopes.last_mut().expect("no scope");
        scope.bindings.insert(name, ty);
    }

    pub fn lookup(&self, name: Name) -> Option<&Type> {
        // Search from innermost to outermost
        for scope in self.scopes.iter().rev() {
            if let Some(ty) = scope.bindings.get(&name) {
                return Some(ty);
            }
        }
        None
    }

    pub fn is_defined(&self, name: Name) -> bool {
        self.lookup(name).is_some()
    }
}

Shadowing

Variables in inner scopes shadow outer ones:

let x = 1       // x : Int in outer scope
let result = run(
    let x = "hello",  // x : String in inner scope
    len(collection: x),  // uses inner x
)
// x : Int (outer still visible)
// In infer_run:
self.env.push_scope();
self.env.bind(x_name, Type::String);  // Shadows outer x
let len_result = self.infer_expr(len_call);
self.env.pop_scope();
// Outer x is now visible again

Type Schemes

For polymorphic bindings, use type schemes:

pub enum TypeScheme {
    /// Monomorphic type
    Mono(Type),

    /// Polymorphic type: forall vars. type
    Poly {
        vars: Vec<TypeVarId>,
        ty: Type,
    },
}

Instantiation

When using a polymorphic binding, instantiate with fresh variables:

impl TypeEnv {
    pub fn lookup_instantiate(&mut self, name: Name, fresh_var: &mut impl FnMut() -> TypeVarId) -> Option<Type> {
        match self.lookup_scheme(name)? {
            TypeScheme::Mono(ty) => Some(ty.clone()),
            TypeScheme::Poly { vars, ty } => {
                // Create fresh variables for each bound variable
                let subst: HashMap<TypeVarId, Type> = vars
                    .iter()
                    .map(|&v| (v, Type::TypeVar(fresh_var())))
                    .collect();

                Some(ty.apply_subst(&subst))
            }
        }
    }
}

Example:

let id = x -> x  // forall T. T -> T
id(42)           // Instantiate: T0 -> T0, then unify T0=Int
id("hi")         // Instantiate: T1 -> T1, then unify T1=String

Function Scopes

Functions create their own scope with parameters:

fn infer_function(&mut self, func: &Function) -> Type {
    self.env.push_function_scope(func.name);

    // Bind parameters
    for param in &func.params {
        self.env.bind(param.name, param.ty.clone());
    }

    // Infer body type
    let body_ty = self.infer_expr(func.body);

    // Check return type annotation
    if let Some(ret_ty) = &func.ret_type {
        self.unify(&body_ty, ret_ty)?;
    }

    self.env.pop_scope();

    Type::Function {
        params: func.params.iter().map(|p| p.ty.clone()).collect(),
        ret: Box::new(body_ty),
        capabilities: func.capabilities.clone(),
    }
}

For Loop Scopes

For loops bind the iteration variable:

fn infer_for(&mut self, for_expr: &ForExpr) -> Type {
    let iter_ty = self.infer_expr(for_expr.iterable);

    // Extract element type
    let elem_ty = match &iter_ty {
        Type::List(elem) => (**elem).clone(),
        Type::Range(elem) => (**elem).clone(),
        _ => {
            self.error(TypeError::NotIterable(iter_ty));
            return Type::Error;
        }
    };

    self.env.push_scope();
    self.env.bind(for_expr.var, elem_ty);

    let body_ty = self.infer_expr(for_expr.body);

    self.env.pop_scope();

    // for..yield produces a list
    if for_expr.is_yield {
        Type::List(Box::new(body_ty))
    } else {
        Type::Void
    }
}

Error Messages

Track scope kind for better errors:

fn error_undefined(&self, name: Name) -> Type {
    let similar = self.find_similar_names(name);

    let in_scope = match self.current_scope_kind() {
        ScopeKind::Function(fn_name) => format!("in function @{}", fn_name),
        ScopeKind::Lambda => "in lambda".to_string(),
        _ => "".to_string(),
    };

    self.errors.push(TypeError::UndefinedVariable {
        name,
        similar,
        context: in_scope,
    });

    Type::Error
}

Free Type Variables

Find type variables not bound in environment:

impl TypeEnv {
    pub fn free_type_vars(&self) -> HashSet<TypeVarId> {
        self.scopes
            .iter()
            .flat_map(|s| s.bindings.values())
            .flat_map(|ty| ty.free_type_vars())
            .collect()
    }
}

Used for let-generalization to determine which variables can be generalized.