Resolved Types

Every type in the IR is fully resolved. Unlike AST types which use string names, resolved types use IDs that directly reference definitions.

ResolvedType

#![allow(unused)]
fn main() {
pub enum ResolvedType {
    /// Primitive type (String, I32, I64, F32, F64, Boolean, Never)
    Primitive(PrimitiveType),

    /// Reference to a struct definition
    Struct(StructId),

    /// Reference to a trait definition
    Trait(TraitId),

    /// Reference to an enum definition
    Enum(EnumId),

    /// Named tuple type: (name1: T1, name2: T2)
    Tuple(Vec<(String, ResolvedType)>),

    /// Generic type instantiation: Box<String>, Optional<I32>, [I32]
    Generic {
        /// The generic struct, enum or trait.
        base: GenericBase,
        args: Vec<ResolvedType>,
    },

    /// Type parameter (T) inside a generic definition
    TypeParam(String),

    /// Reference to a type in another module
    External {
        module_path: Vec<String>,      // e.g., ["utils", "helpers"]
        name: String,                  // Type name
        kind: ImportedKind,            // Struct, Trait, Enum, Function or ModuleLet
        type_args: Vec<ResolvedType>,  // For generics
    },

    /// Closure / function type: (T1, T2) -> R
    ///
    /// Each element is `(convention, type)`. The convention constrains
    /// the caller of the closure.
    Closure {
        param_tys: Vec<(ParamConvention, ResolvedType)>,
        return_ty: Box<ResolvedType>,
    },

    /// Error placeholder. See below.
    Error,
}
}

The built-in compound types

The IR has no special variant for an array, an optional, a dictionary, a range or a sequence. The prelude declares each one as an ordinary generic definition, and a type of that kind is a Generic over it:

Source typePrelude definitionResolvedType
[T]struct Array<T>Generic { base: Struct(array_id), args: [T] }
a for resultstruct Seq<T>Generic { base: Struct(seq_id), args: [T] }
[K: V]struct Dictionary<K, V>Generic { base: Struct(dictionary_id), args: [K, V] }
a..bstruct Range<T>Generic { base: Struct(range_id), args: [T] }
T?enum Optional<T>Generic { base: Enum(optional_id), args: [T] }

The IR has no optional of an optional. The language has one level of absence, so the argument of an Optional is never an Optional. An index into [I32?] has the type I32?, as the type check says.

The prelude comes first in each module, so these definitions have the lowest ids: Array, Seq, Dictionary and Range are StructId(0) to StructId(3), and Optional is EnumId(0). Do not write these numbers in a backend. Use the accessors on IrModule:

#![allow(unused)]
fn main() {
impl IrModule {
    pub fn prelude_array_id(&self) -> Option<StructId>;
    pub fn prelude_seq_id(&self) -> Option<StructId>;
    pub fn prelude_dictionary_id(&self) -> Option<StructId>;
    pub fn prelude_range_id(&self) -> Option<StructId>;
    pub fn prelude_optional_id(&self) -> Option<EnumId>;

    /// `T` when `ty` is `[T]`, and so on for each carrier.
    pub fn array_element_ty<'a>(&self, ty: &'a ResolvedType) -> Option<&'a ResolvedType>;
    pub fn seq_element_ty<'a>(&self, ty: &'a ResolvedType) -> Option<&'a ResolvedType>;
    pub fn dictionary_kv_ty<'a>(&self, ty: &'a ResolvedType)
        -> Option<(&'a ResolvedType, &'a ResolvedType)>;
    pub fn range_element_ty<'a>(&self, ty: &'a ResolvedType) -> Option<&'a ResolvedType>;
    pub fn optional_inner_ty<'a>(&self, ty: &'a ResolvedType) -> Option<&'a ResolvedType>;

    /// True for `Array`, `Seq`, `Dictionary` and `Range`.
    pub fn is_prelude_struct(&self, id: StructId) -> bool;
    /// True for `Optional`.
    pub fn is_prelude_enum(&self, id: EnumId) -> bool;
    /// The structs and enums without the prelude definitions.
    pub fn user_structs(&self) -> impl Iterator<Item = &IrStruct>;
    pub fn user_enums(&self) -> impl Iterator<Item = &IrEnum>;
}
}

MonomorphisePass does not specialise these five definitions. After the pass, a Generic over one of them is the final shape, and a backend reads the element type from args.

GenericBase

Target of a Generic instantiation: a generic struct, enum, or trait. Traits appear here only inside generic constraints (<T: Foo<X>>) and impl headers (impl Foo<X> for Y). FormaLang has no dynamic dispatch, so a trait base never sits in a value type position. Match exhaustively when you extract the ID.

#![allow(unused)]
fn main() {
pub enum GenericBase {
    Struct(StructId),
    Enum(EnumId),
    Trait(TraitId),
}
}

External

The lowering of each module links in the modules that it imports (see Obtaining the IR). An imported struct, enum or trait is then a local definition with a local id, so a type that names it is Struct, Enum or Trait. The public entry points do not give External for a type that a use imports.

External stays in the IR for backends that build IR by hand or read it from another tool. MonomorphisePass::with_imports can replace each External with a local copy of the imported definition.

Error

The lowering gives Error when it has already recorded a CompilerError for the node, and it must still give the node a type. The compile then returns that error, so a module that reaches a backend holds no Error. A backend can treat Error as unreachable.

Type Resolution Examples

The ids below are the ids in a module with no other definitions: the prelude takes the first ids.

FormaLang TypeResolvedType
StringPrimitive(PrimitiveType::String)
I32 / I64Primitive(PrimitiveType::I32) / Primitive(PrimitiveType::I64)
F32 / F64Primitive(PrimitiveType::F32) / Primitive(PrimitiveType::F64)
BooleanPrimitive(PrimitiveType::Boolean)
NeverPrimitive(PrimitiveType::Never)
User (local struct)Struct(StructId(4))
Named (local trait)Trait(TraitId(0))
Status (local enum)Enum(EnumId(1))
[String]Generic { base: Struct(StructId(0)), args: [Primitive(String)] }
0..10Generic { base: Struct(StructId(3)), args: [Primitive(I32)] }
String?Generic { base: Enum(EnumId(0)), args: [Primitive(String)] }
[[I32]]Generic { base: Struct(StructId(0)), args: [Generic { base: Struct(StructId(0)), args: [Primitive(I32)] }] }
[String: I32]Generic { base: Struct(StructId(2)), args: [Primitive(String), Primitive(I32)] }
Box<String> (local generic struct)Generic { base: Struct(StructId(4)), args: [Primitive(String)] }
Maybe<I32> (local generic enum)Generic { base: Enum(EnumId(1)), args: [Primitive(I32)] }
(x: I32, y: I32)Tuple([("x", Primitive(I32)), ("y", Primitive(I32))])
T (in generic)TypeParam("T")
(String, I32) -> BooleanClosure { param_tys: [(Let, Primitive(String)), (Let, Primitive(I32))], return_ty: Primitive(Boolean) }
(mut I32) -> BooleanClosure { param_tys: [(Mut, Primitive(I32))], return_ty: Primitive(Boolean) }
(sink String) -> BooleanClosure { param_tys: [(Sink, Primitive(String))], return_ty: Primitive(Boolean) }

Display Names

#![allow(unused)]
fn main() {
impl ResolvedType {
    /// A display name for this type, for debug output and messages.
    pub fn display_name(&self, module: &IrModule) -> String;
}

// Example
let ty = &field.ty;
println!("Field type: {}", ty.display_name(&module));
// Output: "[String]" or "User" or "Box<I32>"
}

display_name writes the source form of a built-in carrier: [T], T?, [K: V] and T..T. A sequence has the form Seq<T>.