// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Gleam contributors use std::{collections::HashMap, ops::Deref}; use ecow::EcoString; use serde::Serialize; #[cfg(test)] mod tests; use crate::{ io::ordered_map, type_::{ self, Deprecation, Opaque, Type, TypeConstructor, TypeVar, TypeVariantConstructors, ValueConstructorVariant, expression::Implementations, }, }; use crate::build::Package; /// The public interface of a package that gets serialised as a json object. #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct PackageInterface { name: EcoString, version: EcoString, /// The Gleam version constraint that the package specifies in its `gleam.toml`. gleam_version_constraint: Option, /// A map from module name to its interface. #[serde(serialize_with = "ordered_map")] modules: HashMap, } #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct ModuleInterface { /// A vector with the lines composing the module's documentation (that is /// every line preceded by a `////`). documentation: Vec, /// A map from type alias name to its interface. #[serde(serialize_with = "ordered_map")] type_aliases: HashMap, /// A map from type name to its interface. #[serde(serialize_with = "ordered_map")] types: HashMap, /// A map from constant name to its interface. #[serde(serialize_with = "ordered_map")] constants: HashMap, /// A map from function name to its interface. #[serde(serialize_with = "ordered_map")] functions: HashMap, } #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct TypeDefinitionInterface { /// The definition's documentation comment (that is every line preceded by /// `///`). documentation: Option, /// If the definition has a deprecation annotation `@deprecated("...")` /// this field will hold the reason of the deprecation. deprecation: Option, /// The number of type variables in the type definition. /// ```gleam /// /// This type has 2 type variables. /// type Result(a, b) { /// Ok(a) /// Error(b) /// } /// ``` parameters: usize, /// A list of the type constructors. If the type is marked as opaque it /// won't have any visible constructors. constructors: Vec, } #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct TypeConstructorInterface { /// The constructor's documentation comment (that is every line preceded by /// `///`). documentation: Option, /// The name of the type constructor. /// ```gleam /// pub type Box(a) { /// MyBox(value: a) /// //^^^^^ This is the constructor's name /// } /// ``` name: EcoString, /// A list of the parameters needed by the constructor. /// ```gleam /// pub type Box(a) { /// MyBox(value: a) /// // ^^^^^^^^ This is the constructor's parameter. /// } /// ``` parameters: Vec, } #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct TypeAliasInterface { /// The constructor's documentation comment (that is every line preceded by /// `///`). documentation: Option, /// If the alias has a deprecation annotation `@deprecated("...")` /// this field will hold the reason of the deprecation. deprecation: Option, /// The number of type variables in the type alias definition. /// ```gleam /// /// This type alias has 2 type variables. /// type Results(a, b) = List(Restul(a, b)) /// ``` parameters: usize, /// The aliased type. /// ```gleam /// type Ints = List(Int) /// // ^^^^^^^^^ This is the aliased type in a type alias. /// ``` alias: TypeInterface, } #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct ConstantInterface { /// The constant's documentation comment (that is every line preceded by /// `///`). documentation: Option, /// If the constant has a deprecation annotation `@deprecated("...")` /// this field will hold the reason of the deprecation. deprecation: Option, implementations: ImplementationsInterface, /// The constant's type. #[serde(rename = "type")] type_: TypeInterface, } /// A module's function. This differs from a simple `Fn` type as its arguments /// can be labelled. #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct FunctionInterface { /// The function's documentation comment (that is every line preceded by /// `///`). documentation: Option, /// If the constant has a deprecation annotation `@deprecated("...")` /// this field will hold the reason of the deprecation. deprecation: Option, implementations: ImplementationsInterface, parameters: Vec, #[serde(rename = "return")] return_: TypeInterface, } /// Informations about how a value is implemented. #[derive(Debug, Serialize, Copy, Clone)] #[serde(rename_all = "kebab-case")] pub struct ImplementationsInterface { /// Set to `true` if the const/function has a pure Gleam implementation /// (that is, it never uses external code). /// Being pure Gleam means that the function will support all Gleam /// targets, even future ones that are not present to this day. /// /// Consider the following function: /// /// ```gleam /// @external(erlang, "wibble", "wobble") /// pub fn a_random_number() -> Int { /// 4 /// // This is a default implementation. /// } /// ``` /// /// The implementations for this function will look like this: /// /// ```json /// { /// gleam: true, /// can_run_on_erlang: true, /// can_run_on_javascript: true, /// uses_erlang_externals: true, /// uses_javascript_externals: false, /// } /// ``` /// /// - `gleam: true` means that the function has a pure Gleam implementation /// and thus it can be used on all Gleam targets with no problems. /// - `can_run_on_erlang: false` the function can be called on the Erlang /// target. /// - `can_run_on_javascript: true` the function can be called on the JavaScript /// target. /// - `uses_erlang_externals: true` means that the function will use Erlang /// external code when compiled to the Erlang target. /// - `uses_javascript_externals: false` means that the function won't use /// JavaScript external code when compiled to JavaScript. The function can /// still be used on the JavaScript target since it has a pure Gleam /// implementation. gleam: bool, /// Set to `true` if the const/function is defined using Erlang external /// code. That means that the function will use Erlang code through FFI when /// compiled for the Erlang target. uses_erlang_externals: bool, /// Set to `true` if the const/function is defined using JavaScript external /// code. That means that the function will use JavaScript code through FFI /// when compiled for the JavaScript target. /// /// Let's have a look at an example: /// /// ```gleam /// @external(javascript, "wibble", "wobble") /// pub fn javascript_only() -> Int /// ``` /// /// It's implementations field will look like this: /// /// ```json /// { /// gleam: false, /// can_run_on_erlang: false, /// can_run_on_javascript: true, /// uses_erlang_externals: false, /// uses_javascript_externals: true, /// } /// ``` /// /// - `gleam: false` means that the function doesn't have a pure Gleam /// implementations. This means that the function is only defined using /// externals and can only be used on some targets. /// - `can_run_on_erlang: false` the function cannot be called on the Erlang /// target. /// - `can_run_on_javascript: true` the function can be called on the JavaScript /// target. /// - `uses_erlang_externals: false` the function is not using external /// Erlang code. /// - `uses_javascript_externals: true` the function is using JavaScript /// external code. uses_javascript_externals: bool, /// Set to `true` if the const/function is defined using Cranelift external /// code. That means that the function will use Cranelift code through FFI when /// compiled for the Cranelift target. uses_cranelift_externals: bool, /// Whether the function can be called on the Erlang target, either due to a /// pure Gleam implementation or an implementation that uses some Erlang /// externals. can_run_on_erlang: bool, /// Whether the function can be called on the JavaScript target, either due /// to a pure Gleam implementation or an implementation that uses some /// JavaScript externals. can_run_on_javascript: bool, /// Whether the function can be called on the Cranelift target, either due to a /// pure Gleam implementation or an implementation that uses some Cranelift /// externals. can_run_on_cranelift: bool, } impl ImplementationsInterface { fn from_implementations(implementations: &Implementations) -> ImplementationsInterface { // It might look a bit silly to just recreate an identical structure with // a different name. However, this way we won't inadvertently cause breaking // changes if we were to change the names used by the `Implementations` struct // that is used by the target tracking algorithm. // By doing this we can change the target tracking and package interface // separately! // // This pattern matching makes sure we will remember to handle any change // in the `Implementations` struct. let Implementations { gleam, uses_erlang_externals, uses_javascript_externals, uses_cranelift_externals, can_run_on_erlang, can_run_on_javascript, can_run_on_cranelift, } = implementations; ImplementationsInterface { gleam: *gleam, uses_erlang_externals: *uses_erlang_externals, uses_javascript_externals: *uses_javascript_externals, uses_cranelift_externals: *uses_cranelift_externals, can_run_on_erlang: *can_run_on_erlang, can_run_on_javascript: *can_run_on_javascript, can_run_on_cranelift: *can_run_on_cranelift, } } } #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct DeprecationInterface { /// The reason for the deprecation. message: EcoString, } impl DeprecationInterface { fn from_deprecation(deprecation: &Deprecation) -> Option { match deprecation { Deprecation::NotDeprecated => None, Deprecation::Deprecated { message } => Some(DeprecationInterface { message: message.clone(), }), } } } #[derive(Serialize, Debug)] #[serde(tag = "kind")] #[serde(rename_all = "kebab-case")] pub enum TypeInterface { /// A tuple type like `#(Int, Float)`. Tuple { /// The types composing the tuple. elements: Vec, }, /// A function type like `fn(Int, String) -> String`. Fn { parameters: Vec, #[serde(rename = "return")] return_: Box, }, /// A type variable. /// ```gleam /// pub fn wibble(value: a) -> a {} /// // ^ This is a type variable. /// ``` Variable { id: u64 }, /// A custom named type. /// ```gleam /// let value: Bool = True /// ^^^^ This is a named type. /// ``` /// /// All prelude types - like Bool, String, etc. - are named types as well. /// In that case their package is an empty string `""` and their module /// name is the string `"gleam"`. /// Named { name: EcoString, /// The package the type is defined in. package: EcoString, /// The module the type is defined in. module: EcoString, /// The type parameters that might be needed to define a named type. /// ```gleam /// let result: Result(Int, e) = Ok(1) /// // ^^^^^^ The `Result` named type has 2 parameters. /// // In this case it's the Int type and a type /// // variable. /// ``` parameters: Vec, }, } #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct ParameterInterface { /// If the parameter is labelled this will hold the label's name. /// ```gleam /// pub fn repeat(times n: Int) -> List(Int) /// // ^^^^^ This is the parameter's label. /// ``` label: Option, /// The parameter's type. /// ```gleam /// pub fn repeat(times n: Int) -> List(Int) /// // ^^^ This is the parameter's type. /// ``` #[serde(rename = "type")] type_: TypeInterface, } impl PackageInterface { pub fn from_package( package: &Package, cached_modules: &im::HashMap, ) -> PackageInterface { PackageInterface { name: package.config.name.clone(), version: package.config.version.to_string().into(), gleam_version_constraint: package .config .gleam_version .clone() .map(|version| EcoString::from(version.hex().to_string())), modules: package .modules .iter() .map(|module| &module.ast.type_info) .chain( package .cached_module_names .iter() .filter_map(|name| cached_modules.get(name)), ) .filter(|module| !package.config.is_internal_module(module.name.as_str())) .map(|module| (module.name.clone(), ModuleInterface::from_interface(module))) .collect(), } } } impl ModuleInterface { fn from_interface(interface: &type_::ModuleInterface) -> ModuleInterface { let mut types = HashMap::new(); let mut type_aliases = HashMap::new(); let mut constants = HashMap::new(); let mut functions = HashMap::new(); for (name, constructor) in interface.types.iter().filter(|(name, c)| { // Aliases are stored separately c.publicity.is_public() && !interface.type_aliases.contains_key(*name) }) { let mut id_map = IdMap::new(); let TypeConstructor { deprecation, documentation, .. } = constructor; for typed_parameter in &constructor.parameters { id_map.add_type_variable_id(typed_parameter.as_ref()); } let _ = types.insert( name.clone(), TypeDefinitionInterface { documentation: documentation.clone(), deprecation: DeprecationInterface::from_deprecation(deprecation), parameters: interface .types .get(&name.clone()) .map_or(vec![], |t| t.parameters.clone()) .len(), constructors: match interface.types_value_constructors.get(&name.clone()) { Some(TypeVariantConstructors { variants, opaque: Opaque::NotOpaque, .. }) => variants .iter() .map(|constructor| TypeConstructorInterface { documentation: constructor.documentation.clone(), name: constructor.name.clone(), parameters: constructor .parameters .iter() .map(|arg| ParameterInterface { label: arg.label.clone(), // We share the same id_map between each step so that the // incremental ids assigned are consisten with each other type_: from_type_with_ids(arg.type_.as_ref(), &mut id_map), }) .collect(), }) .collect(), Some(_) | None => Vec::new(), }, }, ); } for (name, alias) in interface .type_aliases .iter() .filter(|(_, v)| v.publicity.is_public()) { let mut id_map = IdMap::new(); for typed_parameter in alias.parameters.iter() { id_map.add_type_variable_id(typed_parameter.as_ref()); } let _ = type_aliases.insert( name.clone(), TypeAliasInterface { documentation: alias.documentation.clone(), deprecation: DeprecationInterface::from_deprecation(&alias.deprecation), parameters: alias.arity, alias: from_type_with_ids(&alias.type_, &mut id_map), }, ); } for (name, value) in interface .values .iter() .filter(|(_, v)| v.publicity.is_public()) { match (value.type_.as_ref(), value.variant.clone()) { ( Type::Fn { arguments, return_: return_type, }, ValueConstructorVariant::ModuleFn { documentation, implementations, field_map, .. }, ) => { let mut id_map = IdMap::new(); let reverse_field_map = field_map .as_ref() .map(|field_map| field_map.indices_to_labels()) .unwrap_or_default(); let _ = functions.insert( name.clone(), FunctionInterface { implementations: ImplementationsInterface::from_implementations( &implementations, ), deprecation: DeprecationInterface::from_deprecation(&value.deprecation), documentation, parameters: arguments .iter() .enumerate() .map(|(index, type_)| ParameterInterface { label: reverse_field_map .get(&(index as u32)) .map(|label| (*label).clone()), type_: from_type_with_ids(type_, &mut id_map), }) .collect(), return_: from_type_with_ids(return_type, &mut id_map), }, ); } ( type_, ValueConstructorVariant::ModuleConstant { documentation, implementations, .. }, ) => { let _ = constants.insert( name.clone(), ConstantInterface { implementations: ImplementationsInterface::from_implementations( &implementations, ), type_: TypeInterface::from_type(type_), deprecation: DeprecationInterface::from_deprecation(&value.deprecation), documentation, }, ); } _ => {} } } ModuleInterface { documentation: interface.documentation.clone(), types, type_aliases, constants, functions, } } } impl TypeInterface { fn from_type(type_: &Type) -> TypeInterface { from_type_with_ids(type_, &mut IdMap::new()) } } /// Turns a type into its interface, an `IdMap` is needed to make sure that all /// the type variables' ids that appear in the type are mapped to an incremental /// number and consistent with each other (that is, two types variables that /// have the same id will also have the same incremental number in the end). fn from_type_with_ids(type_: &Type, id_map: &mut IdMap) -> TypeInterface { match type_ { Type::Fn { arguments, return_ } => TypeInterface::Fn { parameters: arguments .iter() .map(|argument| from_type_with_ids(argument.as_ref(), id_map)) .collect(), return_: Box::new(from_type_with_ids(return_, id_map)), }, Type::Tuple { elements } => TypeInterface::Tuple { elements: elements .iter() .map(|element| from_type_with_ids(element.as_ref(), id_map)) .collect(), }, Type::Var { type_ } => match type_ .as_ref() .try_borrow() .expect("borrow type after inference") .deref() { TypeVar::Link { type_ } => from_type_with_ids(type_, id_map), // Since package serialisation happens after inference there // should be no unbound type variables. // TODO: This branch should be `unreachable!()` but because of // https://github.com/gleam-lang/gleam/issues/2533 // we sometimes end up with those in top level // definitions. // However, `Unbound` and `Generic` ids are generated // using the same generator so we have no problem treating // unbound variables as generic ones since ids will never // overlap. // Once #2533 is closed this branch can be turned back to // be unreachable!(). TypeVar::Unbound { id } | TypeVar::Generic { id } => TypeInterface::Variable { id: id_map.map_id(*id), }, }, Type::Named { name, module, arguments, package, .. } => TypeInterface::Named { name: name.clone(), package: package.clone(), module: module.clone(), parameters: arguments .iter() .map(|argument| from_type_with_ids(argument.as_ref(), id_map)) .collect(), }, } } /// This is a map that is used to map type variable id's to progressive numbers /// starting from 0. /// After type inference the ids associated with type variables can be quite /// high and are not the best to produce a human/machine readable output. /// /// Imagine a function like this one: `pub fn wibble(item: a, rest: b) -> c` /// What we want here is for type variables to have increasing ids starting from /// 0: `a` with id `0`, `b` with id `1` and `c` with id `2`. /// /// This map allows us to keep track of the ids we've run into and map those to /// their incremental counterpart starting from 0. struct IdMap { next_id: u64, ids: HashMap, } impl IdMap { /// Create a new map that will assign id numbers starting from 0. fn new() -> IdMap { IdMap { next_id: 0, ids: HashMap::new(), } } /// Map an id to its mapped counterpart starting from 0. If an id has never /// been seen before it will be assigned a new incremental number. fn map_id(&mut self, id: u64) -> u64 { match self.ids.get(&id) { Some(mapped_id) => *mapped_id, None => { let mapped_id = self.next_id; let _ = self.ids.insert(id, mapped_id); self.next_id += 1; mapped_id } } } /// If the type is a type variable, and has not been seen before, it will /// be assigned to a new incremental number. fn add_type_variable_id(&mut self, type_: &Type) { match type_ { // These types have no id to add to the map. Type::Named { .. } | Type::Fn { .. } | Type::Tuple { .. } => (), // If the type is actually a type variable whose id needs to be mapped. Type::Var { type_ } => match type_ .as_ref() .try_borrow() .expect("borrow type after inference") .deref() { TypeVar::Link { .. } => (), TypeVar::Unbound { id } | TypeVar::Generic { id } => { let _ = self.map_id(*id); } }, } } }