diff --git a/compiler-core/src/ast/constant.rs b/compiler-core/src/ast/constant.rs new file mode 100644 index 0000000..dbc71ec --- /dev/null +++ b/compiler-core/src/ast/constant.rs @@ -0,0 +1,675 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2020 The Gleam contributors + +use super::*; +use crate::analyse::Inferred; +use crate::type_::{FieldMap, HasType}; + +pub type TypedConstant = Constant>; +pub type UntypedConstant = Constant<()>; + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum Constant { + Int { + location: SrcSpan, + value: EcoString, + int_value: BigInt, + }, + + Float { + location: SrcSpan, + value: EcoString, + float_value: LiteralFloatValue, + }, + + String { + location: SrcSpan, + value: EcoString, + }, + + Tuple { + location: SrcSpan, + elements: Vec, + type_: T, + }, + + List { + location: SrcSpan, + elements: Vec, + type_: T, + tail: Option>, + }, + + Record { + location: SrcSpan, + arguments_start_position: u32, + module: Option<(EcoString, SrcSpan)>, + name: EcoString, + /// These are the arguments used when calling the record. + /// If the record is not being called to build a value, then this will + /// be `None`. A couple of examples: + /// ```gleam + /// pub const a = Wibble + /// // arguments: None + /// + /// pub const b = Wibble() + /// // arguments: Some(vec![]) + /// + /// pub const c = Wibble(1, 2) + /// // arguments: Some(vec![1, 2]) + /// ``` + arguments: Option>>, + type_: T, + field_map: Inferred, + record_constructor: Option>, + }, + + RecordUpdate { + location: SrcSpan, + constructor_location: SrcSpan, + module: Option<(EcoString, SrcSpan)>, + name: EcoString, + record: RecordBeingUpdated, + arguments: Vec>, + type_: T, + field_map: Inferred, + }, + + BitArray { + location: SrcSpan, + segments: Vec>, + }, + + Var { + location: SrcSpan, + module: Option<(EcoString, SrcSpan)>, + name: EcoString, + constructor: Option>, + type_: T, + }, + + /// A constant binary operation. + /// Most binops are not supported in constants yet, however we parse them + /// and produce an error during type analysis rather than during parsing so + /// we can get better error messages and language server code actions. + BinaryOperator { + location: SrcSpan, + operator_start: u32, + operator: BinOp, + left: Box, + right: Box, + type_: T, + }, + + /// A placeholder constant used to allow module analysis to continue + /// even when there are type errors. Should never end up in generated code. + Invalid { + location: SrcSpan, + type_: T, + /// Extra information about the invalid expression, useful for providing + /// addition help or information, such as code actions to fix invalid + /// states. + extra_information: Option, + }, + + Todo { + location: SrcSpan, + type_: T, + message: Option>, + }, +} + +impl Constant { + pub fn bin_op_precedence(&self) -> u8 { + match self { + Self::BinaryOperator { operator, .. } => operator.precedence(), + Self::Int { .. } + | Self::Float { .. } + | Self::String { .. } + | Self::Var { .. } + | Self::List { .. } + | Self::Tuple { .. } + | Self::Todo { .. } + | Self::BitArray { .. } + | Self::Invalid { .. } + | Self::Record { .. } + | Self::RecordUpdate { .. } => u8::MAX, + } + } + + pub fn bin_op_name(&self) -> Option { + if let Self::BinaryOperator { operator, .. } = self { + Some(*operator) + } else { + None + } + } + + pub fn is_tuple(&self) -> bool { + match self { + Constant::Tuple { .. } => true, + + Constant::Int { .. } + | Constant::Float { .. } + | Constant::String { .. } + | Constant::List { .. } + | Constant::Record { .. } + | Constant::RecordUpdate { .. } + | Constant::BitArray { .. } + | Constant::Var { .. } + | Constant::BinaryOperator { .. } + | Constant::Invalid { .. } + | Constant::Todo { .. } => false, + } + } + + pub fn is_binop(&self) -> bool { + match self { + Constant::BinaryOperator { .. } => true, + + Constant::Tuple { .. } + | Constant::Int { .. } + | Constant::Float { .. } + | Constant::String { .. } + | Constant::List { .. } + | Constant::Record { .. } + | Constant::RecordUpdate { .. } + | Constant::BitArray { .. } + | Constant::Var { .. } + | Constant::Invalid { .. } + | Constant::Todo { .. } => false, + } + } +} + +impl TypedConstant { + pub fn type_(&self) -> Arc { + match self { + Constant::Int { .. } => type_::int(), + Constant::Float { .. } => type_::float(), + Constant::String { .. } => type_::string(), + Constant::BitArray { .. } => type_::bit_array(), + + Constant::List { type_, .. } + | Constant::BinaryOperator { type_, .. } + | Constant::Tuple { type_, .. } + | Constant::Record { type_, .. } + | Constant::RecordUpdate { type_, .. } + | Constant::Var { type_, .. } + | Constant::Todo { type_, .. } + | Constant::Invalid { type_, .. } => type_.clone(), + } + } + + pub fn find_node(&self, byte_index: u32) -> Option> { + if !self.location().contains(byte_index) { + return None; + } + Some(match self { + Constant::Int { .. } + | Constant::Float { .. } + | Constant::String { .. } + | Constant::Invalid { .. } => Located::Constant(self), + + Constant::Todo { message, .. } => message + .iter() + .find_map(|message| message.find_node(byte_index)) + .unwrap_or(Located::Constant(self)), + + Constant::Var { + module: Some((module_alias, location)), + constructor: Some(constructor), + .. + } + | Constant::Record { + module: Some((module_alias, location)), + record_constructor: Some(constructor), + .. + } if location.contains(byte_index) => match &constructor.variant { + ValueConstructorVariant::ModuleConstant { module, .. } + | ValueConstructorVariant::ModuleFn { module, .. } + | ValueConstructorVariant::Record { module, .. } => Located::ModuleName { + location: *location, + module_name: module.clone(), + module_alias: module_alias.clone(), + layer: Layer::Value, + }, + ValueConstructorVariant::LocalVariable { .. } => Located::Constant(self), + }, + Constant::Var { .. } => Located::Constant(self), + Constant::Tuple { elements, .. } => elements + .iter() + .find_map(|element| element.find_node(byte_index)) + .unwrap_or(Located::Constant(self)), + Constant::List { elements, tail, .. } => elements + .iter() + .find_map(|element| element.find_node(byte_index)) + .or_else(|| tail.as_deref().and_then(|tail| tail.find_node(byte_index))) + .unwrap_or(Located::Constant(self)), + Constant::Record { + arguments, + type_, + name, + .. + } => arguments + .iter() + .flatten() + .find_map(|argument| argument.find_node(byte_index, type_, name)) + .unwrap_or(Located::Constant(self)), + Constant::RecordUpdate { + record, arguments, .. + } => record + .base + .find_node(byte_index) + .or_else(|| { + arguments + .iter() + .find_map(|arg| arg.value.find_node(byte_index)) + }) + .unwrap_or(Located::Constant(self)), + Constant::BitArray { segments, .. } => segments + .iter() + .find_map(|segment| segment.find_node(byte_index)) + .unwrap_or(Located::Constant(self)), + Constant::BinaryOperator { left, right, .. } => left + .find_node(byte_index) + .or_else(|| right.find_node(byte_index)) + .unwrap_or(Located::Constant(self)), + }) + } + + pub fn definition_location(&self) -> Option { + match self { + Constant::Int { .. } + | Constant::Float { .. } + | Constant::String { .. } + | Constant::Tuple { .. } + | Constant::List { .. } + | Constant::BitArray { .. } + | Constant::Todo { .. } + | Constant::BinaryOperator { .. } + | Constant::Invalid { .. } => None, + Constant::Record { + record_constructor: value_constructor, + .. + } + | Constant::Var { + constructor: value_constructor, + .. + } => value_constructor + .as_ref() + .map(|constructor| constructor.definition_location()), + Constant::RecordUpdate { .. } => None, + } + } + + pub(crate) fn referenced_variables(&self) -> imbl::HashSet<&EcoString> { + match self { + Constant::Var { name, .. } => imbl::hashset![name], + + Constant::Invalid { .. } + | Constant::Int { .. } + | Constant::Float { .. } + | Constant::String { .. } => imbl::hashset![], + + Constant::Todo { message, .. } => message + .as_ref() + .map(|message| message.referenced_variables()) + .unwrap_or(imbl::hashset![]), + + Constant::Tuple { elements, .. } => elements + .iter() + .map(|element| element.referenced_variables()) + .fold(imbl::hashset![], imbl::HashSet::union), + + Constant::List { elements, tail, .. } => elements + .iter() + .map(|element| element.referenced_variables()) + .chain(tail.iter().map(|tail| tail.referenced_variables())) + .fold(imbl::hashset![], imbl::HashSet::union), + + Constant::Record { arguments, .. } => arguments + .iter() + .flatten() + .map(|argument| argument.value.referenced_variables()) + .fold(imbl::hashset![], imbl::HashSet::union), + + Constant::RecordUpdate { + record, arguments, .. + } => record.base.referenced_variables().union( + arguments + .iter() + .map(|arg| arg.value.referenced_variables()) + .fold(imbl::hashset![], imbl::HashSet::union), + ), + + Constant::BitArray { segments, .. } => segments + .iter() + .map(|segment| { + segment + .options + .iter() + .map(|option| option.referenced_variables()) + .fold(segment.value.referenced_variables(), imbl::HashSet::union) + }) + .fold(imbl::hashset![], imbl::HashSet::union), + + Constant::BinaryOperator { left, right, .. } => left + .referenced_variables() + .union(right.referenced_variables()), + } + } + + pub(crate) fn syntactically_eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Constant::Todo { message, .. }, + Constant::Todo { + message: other_message, + .. + }, + ) => match (message, other_message) { + (None, None) => true, + (Some(_), None) | (None, Some(_)) => false, + (Some(message), Some(other_message)) => message.syntactically_eq(other_message), + }, + (Constant::Todo { .. }, _) => false, + + (Constant::Int { int_value: n, .. }, Constant::Int { int_value: m, .. }) => n == m, + (Constant::Int { .. }, _) => false, + + (Constant::Float { float_value: n, .. }, Constant::Float { float_value: m, .. }) => { + n == m + } + (Constant::Float { .. }, _) => false, + + ( + Constant::String { value, .. }, + Constant::String { + value: other_value, .. + }, + ) => value == other_value, + (Constant::String { .. }, _) => false, + + ( + Constant::Tuple { elements, .. }, + Constant::Tuple { + elements: other_elements, + .. + }, + ) => pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }), + (Constant::Tuple { .. }, _) => false, + + ( + Constant::List { elements, tail, .. }, + Constant::List { + elements: other_elements, + tail: other_tail, + .. + }, + ) => { + let tails_are_equal = match (tail, other_tail) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(tail), Some(other_tail)) => tail.syntactically_eq(other_tail), + }; + tails_are_equal + && pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }) + } + (Constant::List { .. }, _) => false, + + ( + Constant::Record { + module, + name, + arguments, + .. + }, + Constant::Record { + module: other_module, + name: other_name, + arguments: other_arguments, + .. + }, + ) => { + let modules_are_equal = match (module, other_module) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some((one, _)), Some((other, _))) => one == other, + }; + + let arguments_are_equal = match (arguments, other_arguments) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(arguments), Some(other_arguments)) => { + modules_are_equal + && name == other_name + && pairwise_all(arguments, other_arguments, |(one, other)| { + one.label == other.label && one.value.syntactically_eq(&other.value) + }) + } + }; + + modules_are_equal && arguments_are_equal + } + (Constant::Record { .. }, _) => false, + + ( + Constant::RecordUpdate { + module, + name, + record, + arguments, + .. + }, + Constant::RecordUpdate { + module: other_module, + name: other_name, + record: other_record, + arguments: other_arguments, + .. + }, + ) => { + let modules_are_equal = match (module, other_module) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some((one, _)), Some((other, _))) => one == other, + }; + + modules_are_equal + && name == other_name + && record.base.syntactically_eq(&other_record.base) + && pairwise_all(arguments, other_arguments, |(one, other)| { + one.label == other.label && one.value.syntactically_eq(&other.value) + }) + } + (Constant::RecordUpdate { .. }, _) => false, + + ( + Constant::BitArray { segments, .. }, + Constant::BitArray { + segments: other_segments, + .. + }, + ) => pairwise_all(segments, other_segments, |(one, other)| { + one.syntactically_eq(other) + }), + (Constant::BitArray { .. }, _) => false, + + ( + Constant::Var { module, name, .. }, + Constant::Var { + module: other_module, + name: other_name, + .. + }, + ) => { + let modules_are_equal = match (module, other_module) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some((one, _)), Some((other, _))) => one == other, + }; + + modules_are_equal && name == other_name + } + (Constant::Var { .. }, _) => false, + + ( + Constant::BinaryOperator { + left, + right, + operator, + .. + }, + Constant::BinaryOperator { + left: other_left, + right: other_right, + operator: other_operator, + .. + }, + ) => { + operator == other_operator + && left.syntactically_eq(other_left) + && right.syntactically_eq(other_right) + } + (Constant::BinaryOperator { .. }, _) => false, + + (Constant::Invalid { .. }, _) => false, + } + } + + /// If the constant is a list whose elements are known at compile time, this + /// returns a vec of all its elements. + /// This can happen with: + /// - literal lists: `[1, 2]` + /// - literal lists whose tail is also known at compile time: + /// - `[1, 2, ..[3, 4]]` + /// - `[1, 2, ..a_known_module_constant]` + /// + pub fn list_elements(&self) -> Option> { + match self { + Constant::List { elements, tail, .. } => { + if let Some(tail) = tail { + // There's a tail, if it cannot be known at compile time, + // then this entire list cannot be known at compile time! + let tail_elements = tail.list_elements()?; + Some(elements.iter().chain(tail_elements).collect()) + } else { + // There's no tail, we just return the elements + Some(elements.iter().collect()) + } + } + Constant::Var { + constructor: Some(constructor), + .. + } => match &constructor.variant { + ValueConstructorVariant::ModuleConstant { literal, .. } => literal.list_elements(), + ValueConstructorVariant::LocalVariable { .. } + | ValueConstructorVariant::ModuleFn { .. } + | ValueConstructorVariant::Record { .. } => None, + }, + + Constant::Int { .. } + | Constant::Float { .. } + | Constant::String { .. } + | Constant::Tuple { .. } + | Constant::Record { .. } + | Constant::RecordUpdate { .. } + | Constant::BitArray { .. } + | Constant::BinaryOperator { .. } + | Constant::Var { .. } + | Constant::Todo { .. } + | Constant::Invalid { .. } => None, + } + } + + /// If the constant is a record or record update this returns its tag. + /// It might return `None` if the record constructor couldn't be inferred. + /// For example, if someone wrote a variant that doesn't exist: + /// + /// ```gleam + /// pub const wibble = ThisIsNotDefinedAnywhere(1, 2) + /// ``` + /// + /// In this case the record wouldn't have a constructor, as there's no + /// custom type defining it anywhere! + /// + pub(crate) fn constant_record_tag(&self) -> Option { + if let Constant::Record { + record_constructor: Some(constructor), + .. + } = self + && let ValueConstructorVariant::Record { name, .. } = &constructor.variant + { + Some(name.clone()) + } else if let Constant::RecordUpdate { name, .. } = self { + Some(name.clone()) + } else { + None + } + } +} + +impl HasType for TypedConstant { + fn type_(&self) -> Arc { + self.type_() + } +} + +impl Constant { + pub fn location(&self) -> SrcSpan { + match self { + Constant::Int { location, .. } + | Constant::List { location, .. } + | Constant::Float { location, .. } + | Constant::Tuple { location, .. } + | Constant::String { location, .. } + | Constant::Record { location, .. } + | Constant::RecordUpdate { location, .. } + | Constant::BitArray { location, .. } + | Constant::Var { location, .. } + | Constant::Invalid { location, .. } + | Constant::Todo { location, .. } + | Constant::BinaryOperator { location, .. } => *location, + } + } + + #[must_use] + pub fn can_have_multiple_per_line(&self) -> bool { + match self { + Constant::Int { .. } + | Constant::Float { .. } + | Constant::String { .. } + | Constant::Var { .. } => true, + + Constant::Tuple { .. } + | Constant::Todo { .. } + | Constant::List { .. } + | Constant::Record { .. } + | Constant::RecordUpdate { .. } + | Constant::BitArray { .. } + | Constant::BinaryOperator { .. } + | Constant::Invalid { .. } => false, + } + } +} + +impl HasLocation for Constant { + fn location(&self) -> SrcSpan { + self.location() + } +} + +impl bit_array::GetLiteralValue for Constant { + fn as_int_literal(&self) -> Option { + if let Constant::Int { int_value, .. } = self { + Some(int_value.clone()) + } else { + None + } + } +} diff --git a/compiler-core/src/type_/guard.rs b/compiler-core/src/type_/guard.rs new file mode 100644 index 0000000..0a1ff48 --- /dev/null +++ b/compiler-core/src/type_/guard.rs @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Gleam contributors + +use crate::{ + ast::{BinOp, ClauseGuard, TypedClauseGuard, TypedExpr, UntypedClauseGuard}, + reference::ReferenceKind, + type_::{ + Error, ExprTyper, FieldAccessUsage, ModuleValueConstructor, RecordAccessor, Type, + ValueConstructorVariant, ValueUsage, bool, + error::{FeatureKind, convert_unify_error}, + float, int, string, unify, + }, +}; +use ecow::EcoString; +use src_span::SrcSpan; + +pub struct GuardTyper<'expression_typer, 'env, 'module> { + typer: &'expression_typer mut ExprTyper<'env, 'module>, +} + +impl<'expression_typer, 'env, 'module> GuardTyper<'expression_typer, 'env, 'module> { + pub fn new(typer: &'expression_typer mut ExprTyper<'env, 'module>) -> Self { + Self { typer } + } + + pub fn infer(mut self, guard: UntypedClauseGuard) -> TypedClauseGuard { + self.do_infer(guard) + } + + fn do_infer(&mut self, guard: UntypedClauseGuard) -> TypedClauseGuard { + match guard { + ClauseGuard::Invalid { .. } => { + unreachable!("untyped guard should never be invalid") + } + + ClauseGuard::Var { location, name, .. } => { + match self.infer_clause_guard_variable(name, location) { + Ok(variable) => variable, + Err(error) => { + self.typer.problems.error(error); + ClauseGuard::Invalid { + location, + type_: self.typer.new_unbound_var(), + } + } + } + } + + ClauseGuard::TupleIndex { + location, + tuple, + index, + .. + } => { + let tuple = self.do_infer(*tuple); + let index_type = match tuple.type_().as_ref() { + Type::Tuple { elements } => match elements.get(index as usize) { + Some(type_) => type_.clone(), + // If the index is outside the tuple range, then we + // report the error and return an unbound type to keep + // going. + None => { + self.typer.problems.error(Error::OutOfBoundsTupleIndex { + location, + index, + size: elements.len(), + }); + self.typer.new_unbound_var() + } + }, + + tuple_type if tuple_type.is_unbound() => { + self.typer.problems.error(Error::NotATupleUnbound { + location: tuple.location(), + }); + self.typer.new_unbound_var() + } + + Type::Named { .. } | Type::Fn { .. } | Type::Var { .. } => { + self.typer.problems.error(Error::NotATuple { + location: tuple.location(), + given: tuple.type_(), + }); + self.typer.new_unbound_var() + } + }; + + ClauseGuard::TupleIndex { + location, + index, + type_: index_type, + tuple: Box::new(tuple), + } + } + + ClauseGuard::FieldAccess { + label_location, + label, + container, + index: _, + type_: (), + } => { + let container_location = container.location(); + let result = if let ClauseGuard::Var { name, location, .. } = *container { + // If the container looks like a regular variable, then this + // could either be a module select, or a record access. + match self.infer_clause_guard_variable(name.clone(), location) { + // If the variable itself cannot be inferred as one, then + // it could really be a module select. We try that one + // as an alternative. + Err(error) => self.infer_guard_module_access( + name, + label, + location, + label_location, + error, + ), + // Otherwise that's a proper variable and not a module name, + // so the whole expression has to be inferred as a regular + // record access. + Ok(variable) => { + self.infer_guard_record_access(variable, label, label_location) + } + } + } else { + // If it doesn't this has to be a regular record access and + // we try and infer it as such. + let inferred_container = self.do_infer(*container.clone()); + self.infer_guard_record_access(inferred_container, label, label_location) + }; + + match result { + Ok(inferred) => inferred, + Err(error) => { + self.typer.problems.error(error); + ClauseGuard::Invalid { + location: container_location.merge(&label_location), + type_: self.typer.new_unbound_var(), + } + } + } + } + + ClauseGuard::ModuleSelect { .. } => { + unreachable!("untyped guard should never be module select") + } + + ClauseGuard::Not { + location, + expression, + } => { + let expression = self.do_infer(*expression); + if let Err(error) = unify(bool(), expression.type_()) { + self.typer + .problems + .error(convert_unify_error(error, expression.location())); + } + ClauseGuard::Not { + location, + expression: Box::new(expression), + } + } + + ClauseGuard::Constant(constant) => { + ClauseGuard::Constant(self.typer.infer_const(&None, constant)) + } + + ClauseGuard::Block { value, location } => ClauseGuard::Block { + location, + value: Box::new(self.do_infer(*value)), + }, + + ClauseGuard::BinaryOperator { + location, + operator, + operator_start, + left, + right, + } => { + let left = self.do_infer(*left); + let right = self.do_infer(*right); + + match operator { + BinOp::And | BinOp::Or => { + if let Err(error) = unify(bool(), left.type_()) { + self.typer + .problems + .error(convert_unify_error(error, left.location())); + } + if let Err(error) = unify(bool(), right.type_()) { + self.typer + .problems + .error(convert_unify_error(error, right.location())); + } + } + + BinOp::Eq | BinOp::NotEq => { + if let Err(error) = unify(left.type_(), right.type_()) { + self.typer + .problems + .error(convert_unify_error(error, right.location())); + } + } + + BinOp::GtInt + | BinOp::GtEqInt + | BinOp::LtInt + | BinOp::LtEqInt + | BinOp::AddInt + | BinOp::SubInt + | BinOp::DivInt + | BinOp::MultInt + | BinOp::RemainderInt => { + self.typer + .track_feature_usage(FeatureKind::ArithmeticInGuards, location); + // If both operands are floats, then we use a more specialised + // error. + if left.type_().is_float() && right.type_().is_float() { + self.typer.problems.error(Error::IntOperatorOnFloats { + operator, + location: SrcSpan::new( + operator_start, + operator_start + operator.size(), + ), + }); + } else { + if let Err(error) = unify(int(), left.type_()) { + self.typer + .problems + .error(convert_unify_error(error, left.location())); + } + if let Err(error) = unify(int(), right.type_()) { + self.typer + .problems + .error(convert_unify_error(error, right.location())); + } + } + } + + BinOp::GtFloat + | BinOp::GtEqFloat + | BinOp::LtFloat + | BinOp::LtEqFloat + | BinOp::AddFloat + | BinOp::SubFloat + | BinOp::DivFloat + | BinOp::MultFloat => { + self.typer + .track_feature_usage(FeatureKind::ArithmeticInGuards, location); + + // If both operands are int then we use a more specialised + // error + if left.type_().is_int() && right.type_().is_int() { + self.typer.problems.error(Error::FloatOperatorOnInts { + operator, + location: SrcSpan::new( + operator_start, + operator_start + operator.size(), + ), + }); + } else { + if let Err(error) = unify(float(), left.type_()) { + self.typer + .problems + .error(convert_unify_error(error, left.location())); + } + if let Err(error) = unify(float(), right.type_()) { + self.typer + .problems + .error(convert_unify_error(error, right.location())); + } + } + } + + BinOp::Concatenate => { + self.typer + .track_feature_usage(FeatureKind::ConcatenateInGuards, location); + + if let Err(error) = unify(string(), left.type_()) { + self.typer + .problems + .error(convert_unify_error(error, left.location())); + } + if let Err(error) = unify(string(), right.type_()) { + self.typer + .problems + .error(convert_unify_error(error, right.location())); + } + } + } + + ClauseGuard::BinaryOperator { + location, + operator, + operator_start, + left: Box::new(left), + right: Box::new(right), + } + } + } + } + + fn infer_clause_guard_variable( + &mut self, + name: EcoString, + location: SrcSpan, + ) -> Result { + let constructor = + self.typer + .infer_value_constructor(&None, &name, &location, ValueUsage::Other)?; + + // We cannot support all values in guard expressions as the BEAM does not + let (definition_location, origin) = match &constructor.variant { + ValueConstructorVariant::LocalVariable { location, origin } => { + (*location, origin.clone()) + } + + ValueConstructorVariant::ModuleFn { .. } | ValueConstructorVariant::Record { .. } => { + return Err(Error::NonLocalClauseGuardVariable { location, name }); + } + + ValueConstructorVariant::ModuleConstant { literal, .. } => { + return Ok(ClauseGuard::Constant(literal.clone())); + } + }; + + Ok(ClauseGuard::Var { + location, + name, + origin, + type_: constructor.type_, + definition_location, + }) + } + + fn infer_guard_record_access( + &mut self, + container: TypedClauseGuard, + label: EcoString, + location: SrcSpan, + ) -> Result { + let container = Box::new(container); + let container_type = container.type_(); + let RecordAccessor { + index, + label, + type_, + documentation: _, + } = self.typer.infer_known_record_access( + container_type, + container.location(), + FieldAccessUsage::Other, + location, + label, + )?; + Ok(ClauseGuard::FieldAccess { + container, + label, + index: Some(index), + label_location: location, + type_, + }) + } + + fn infer_guard_module_access( + &mut self, + name: EcoString, + label: EcoString, + module_location: SrcSpan, + label_location: SrcSpan, + record_access_error: Error, + ) -> Result { + let module_access = self + .typer + .infer_module_access(&name, label, &module_location, label_location) + .and_then(|module_select| { + if let TypedExpr::ModuleSelect { + location, + field_start: _, + type_, + label, + module_name, + module_alias, + constructor, + } = module_select + { + match constructor { + ModuleValueConstructor::Constant { + literal, + location: definition_location, + .. + } => { + self.typer.environment.references.register_value_reference( + module_name.clone(), + label.clone(), + &label, + label_location, + ReferenceKind::Qualified { + module_alias: module_alias.clone(), + module_location, + }, + ); + + Ok(ClauseGuard::ModuleSelect { + location, + field_start: label_location.start, + definition_location, + type_, + label, + module_name, + module_alias, + literal, + }) + } + + ModuleValueConstructor::Record { .. } + | ModuleValueConstructor::Fn { .. } => { + Err(Error::RecordAccessUnknownType { location }) + } + } + } else { + Err(Error::RecordAccessUnknownType { + location: module_location, + }) + } + }); + + // If the name is in the environment, use the original error from + // inferring the record access, so that we can suggest possible + // misspellings of field names + if self.typer.environment.scope.contains_key(&name) { + module_access.map_err(|_| record_access_error) + } else { + module_access + } + } +} diff --git a/compiler-core/src/type_/tests/guards.rs b/compiler-core/src/type_/tests/guards.rs new file mode 100644 index 0000000..aa2afef --- /dev/null +++ b/compiler-core/src/type_/tests/guards.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 The Gleam contributors + +use crate::{assert_module_error, assert_module_infer}; + +#[test] +fn nested_record_access() { + assert_module_infer!( + r#" +pub type A { + A(b: B) +} + +pub type B { + B(c: C) +} + +pub type C { + C(d: Bool) +} + +pub fn a(a: A) { + case a { + _ if a.b.c.d -> 1 + _ -> 0 + } +} +"#, + vec![ + ("A", "fn(B) -> A"), + ("B", "fn(C) -> B"), + ("C", "fn(Bool) -> C"), + ("a", "fn(A) -> Int"), + ], + ); +} + +#[test] +fn string_variable_access() { + assert_module_error!( + r#" +pub fn a(a: String) { + case a { + _ if a.b -> 1 + _ -> 0 + } +} +"# + ); +} + +#[test] +fn qualified_record() { + assert_module_infer!( + ("wibble", "pub type Wibble { Wibble Wobble }"), + " +import wibble + +pub fn main(wibble: wibble.Wibble) { + case wibble { + w if w == wibble.Wobble -> True + _ -> False + } +} +", + vec![("main", "fn(Wibble) -> Bool")] + ); +} + +#[test] +fn qualified_record_with_arguments() { + assert_module_infer!( + ( + "wibble", + "pub type Wibble { Wibble(Int) Wobble(Int, Float) }" + ), + " +import wibble + +pub fn main(wibble: wibble.Wibble) { + case wibble { + w if w == wibble.Wobble(1, 3.8) -> True + _ -> False + } +} +", + vec![("main", "fn(Wibble) -> Bool")] + ); +}