// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Gleam contributors use std::collections::HashMap; use ecow::EcoString; use crate::{ ast::Publicity, type_::printer::{NameContextInformation, Names}, }; use super::{Variable, missing_patterns::Term}; #[derive(Debug)] pub struct Printer<'a> { names: &'a Names, /// This is the module that is being currently analysed. current_module: EcoString, } impl<'a> Printer<'a> { pub fn new(current_module: EcoString, names: &'a Names) -> Self { Printer { current_module, names, } } pub fn print_terms( &self, subjects: &[Variable], terms: &[Term], mapping: &HashMap, ) -> EcoString { let mut buffer = EcoString::new(); for (i, subject) in subjects.iter().enumerate() { if i != 0 { buffer.push_str(", "); } match mapping.get(&subject.id) { Some(&index) => { let term = terms.get(index).expect("Term must exist"); self.print(term, terms, mapping, &mut buffer); } None => buffer.push('_'), } } buffer } fn print( &self, term: &Term, terms: &[Term], mapping: &HashMap, buffer: &mut EcoString, ) { match term { Term::Variant { name, module, fields, variable, } => { let is_defined_in_current_module = *module == self.current_module; let is_internal = variable .type_ .named_type_publicity() .unwrap_or(Publicity::Public) .is_internal(); // We don't want to expose information about an internal type, // making it easy to rely on its internal structure. // So what we do is we just show a catch all pattern `_` for // those. // We do this only if the internal type is defined in a // different module from the one being analysed, otherwise it's // totally fair to want to match on such type. if is_internal && !is_defined_in_current_module { buffer.push('_'); return; } let (module, name) = match self.names.named_constructor(module, name) { NameContextInformation::Qualified(module, name) => (Some(module), name), NameContextInformation::Unqualified(name) => (None, name), NameContextInformation::Unimported(module, name) => { (module.split('/').next_back(), name) } }; if let Some(module) = module { buffer.push_str(module); buffer.push('.'); } buffer.push_str(name); if fields.is_empty() { return; } buffer.push('('); for (i, field) in fields.iter().enumerate() { if i != 0 { buffer.push_str(", "); } let mut has_label = false; if let Some(label) = &field.label { buffer.push_str(label); buffer.push(':'); has_label = true; } if let Some(&idx) = mapping.get(&field.variable.id) { let term = terms.get(idx).expect("Term must exist"); match term { // If it is an infinite term and this field is labelled, it is generally // more useful to print just the label using label shorthand syntax. // For example, printing `Person(name:, age:)` instead of // `Person(name: _, age: _)`. Term::Infinite { .. } if has_label => {} Term::Infinite { .. } | Term::Variant { .. } | Term::Tuple { .. } | Term::EmptyList { .. } | Term::List { .. } => { // If this field has a label, the current buffer looks like `label:`, // so we want to print a space before printing the pattern for it. // If there is no label, we don't need to print the space. if has_label { buffer.push(' '); } self.print(term, terms, mapping, buffer); } } } else if !has_label { buffer.push('_'); } } buffer.push(')'); } Term::Tuple { elements, .. } => { buffer.push_str("#("); for (i, variable) in elements.iter().enumerate() { if i != 0 { buffer.push_str(", "); } if let Some(&idx) = mapping.get(&variable.id) { self.print( terms.get(idx).expect("Term must exist"), terms, mapping, buffer, ); } else { buffer.push('_'); } } buffer.push(')'); } Term::Infinite { .. } => buffer.push('_'), Term::EmptyList { .. } => buffer.push_str("[]"), Term::List { .. } => { buffer.push('['); self.print_list(term, terms, mapping, buffer); buffer.push(']'); } } } fn print_list( &self, term: &Term, terms: &[Term], mapping: &HashMap, buffer: &mut EcoString, ) { match term { Term::Infinite { .. } | Term::Variant { .. } | Term::Tuple { .. } => buffer.push('_'), Term::EmptyList { .. } => {} Term::List { first, rest, .. } => { if let Some(&idx) = mapping.get(&first.id) { self.print( terms.get(idx).expect("Term must exist"), terms, mapping, buffer, ) } else { buffer.push('_'); } if let Some(&idx) = mapping.get(&rest.id) { let term = terms.get(idx).expect("Term must exist"); match term { Term::EmptyList { .. } => {} Term::Variant { .. } | Term::Tuple { .. } | Term::Infinite { .. } | Term::List { .. } => { buffer.push_str(", "); self.print_list(term, terms, mapping, buffer) } } } else { buffer.push_str(", .."); } } } } } #[cfg(test)] mod tests { use ecow::EcoString; use super::Printer; use std::{collections::HashMap, sync::Arc}; use crate::{ ast::SrcSpan, exhaustiveness::{ Variable, missing_patterns::{Term, VariantField}, }, type_::{Type, printer::Names}, }; /// Create a variable with a dummy type, for ease of writing tests fn make_variable(id: usize) -> Variable { Variable { id, type_: Arc::new(Type::Tuple { elements: Vec::new(), }), } } fn field(variable: Variable, label: Option<&str>) -> VariantField { VariantField { variable, label: label.map(EcoString::from), } } fn get_mapping(terms: &[Term]) -> HashMap { let mut mapping: HashMap = HashMap::new(); for (index, term) in terms.iter().enumerate() { _ = mapping.insert(term.variable().id, index); } mapping } #[test] fn test_value_in_current_module() { let current_module = EcoString::from("module"); let mut names = Names::new(); names.named_constructor_in_scope(current_module.clone(), "Wibble".into(), "Wibble".into()); let printer = Printer::new(current_module.clone(), &names); let subjects = &[make_variable(0)]; let term = Term::Variant { variable: subjects[0].clone(), name: "Wibble".into(), module: current_module, fields: Vec::new(), }; let terms = &[term]; let mapping = get_mapping(terms); assert_eq!(printer.print_terms(subjects, terms, &mapping), "Wibble"); } #[test] fn test_value_in_current_module_with_arguments() { let current_module = EcoString::from("module"); let mut names = Names::new(); names.named_constructor_in_scope(current_module.clone(), "Wibble".into(), "Wibble".into()); let printer = Printer::new(current_module.clone(), &names); let var1 = make_variable(1); let var2 = make_variable(2); let subjects = &[make_variable(0)]; let term = Term::Variant { variable: subjects[0].clone(), name: "Wibble".into(), module: current_module, fields: vec![field(var1.clone(), None), field(var2.clone(), None)], }; let terms = &[ term, Term::EmptyList { variable: var1 }, Term::Infinite { variable: var2 }, ]; let mapping = get_mapping(terms); assert_eq!( printer.print_terms(subjects, terms, &mapping), "Wibble([], _)" ); } #[test] fn test_value_in_current_module_with_labelled_arguments() { let current_module = EcoString::from("module"); let mut names = Names::new(); names.named_constructor_in_scope(current_module.clone(), "Wibble".into(), "Wibble".into()); let printer = Printer::new(current_module.clone(), &names); let var1 = make_variable(1); let var2 = make_variable(2); let subjects = &[make_variable(0)]; let term = Term::Variant { variable: subjects[0].clone(), name: "Wibble".into(), module: current_module, fields: vec![ field(var1.clone(), Some("list")), field(var2.clone(), Some("other")), ], }; let terms = &[ term, Term::EmptyList { variable: var1 }, Term::Infinite { variable: var2 }, ]; let mapping = get_mapping(terms); assert_eq!( printer.print_terms(subjects, terms, &mapping), "Wibble(list: [], other:)" ); } #[test] fn test_module_alias() { let mut names = Names::new(); assert!( names .imported_module("mod".into(), "shapes".into(), SrcSpan::new(50, 60)) .is_none() ); let printer = Printer::new("module".into(), &names); let subjects = &[make_variable(0)]; let term = Term::Variant { variable: subjects[0].clone(), name: "Rectangle".into(), module: "mod".into(), fields: Vec::new(), }; let terms = &[term]; let mapping = get_mapping(terms); assert_eq!( printer.print_terms(subjects, terms, &mapping), "shapes.Rectangle" ); } #[test] fn test_unqualified_value() { let mut names = Names::new(); names.named_constructor_in_scope("regex".into(), "Regex".into(), "Regex".into()); let printer = Printer::new("module".into(), &names); let arg = make_variable(1); let subjects = &[make_variable(0)]; let term = Term::Variant { variable: subjects[0].clone(), name: "Regex".into(), module: "regex".into(), fields: vec![field(arg.clone(), None)], }; let terms = &[term, Term::Infinite { variable: arg }]; let mapping = get_mapping(terms); assert_eq!(printer.print_terms(subjects, terms, &mapping), "Regex(_)"); } #[test] fn test_unqualified_value_with_alias() { let mut names = Names::new(); names.named_constructor_in_scope("regex".into(), "Regex".into(), "Reg".into()); names.named_constructor_in_scope("gleam".into(), "None".into(), "None".into()); let printer = Printer::new("current_module".into(), &names); let arg = make_variable(1); let subjects = &[make_variable(0)]; let term = Term::Variant { variable: subjects[0].clone(), name: "Regex".into(), module: "regex".into(), fields: vec![field(arg.clone(), None)], }; let terms = &[ term, Term::Variant { variable: arg, name: "None".into(), module: "gleam".into(), fields: vec![], }, ]; let mapping = get_mapping(terms); assert_eq!(printer.print_terms(subjects, terms, &mapping), "Reg(None)"); } #[test] fn test_list_pattern() { let mut names = Names::new(); names.named_constructor_in_scope("module".into(), "Type".into(), "Type".into()); let printer = Printer::new("module".into(), &names); let var1 = make_variable(1); let var2 = make_variable(2); let var3 = make_variable(3); let subjects = &[make_variable(0)]; let term = Term::List { variable: subjects[0].clone(), first: var1.clone(), rest: var2.clone(), }; let terms = &[ term, Term::Variant { variable: var1, name: "Type".into(), module: "module".into(), fields: Vec::new(), }, Term::List { variable: var2, first: var3.clone(), rest: make_variable(4), }, Term::Infinite { variable: var3 }, ]; let mapping = get_mapping(terms); assert_eq!( printer.print_terms(subjects, terms, &mapping), "[Type, _, ..]" ); } #[test] fn test_multi_pattern() { let mut names = Names::new(); names.named_constructor_in_scope("gleam".into(), "Ok".into(), "Ok".into()); names.named_constructor_in_scope("gleam".into(), "False".into(), "False".into()); let printer = Printer::new("module".into(), &names); let subjects = &[make_variable(0), make_variable(1), make_variable(2)]; let terms = &[ Term::Variant { variable: subjects[0].clone(), name: "Ok".into(), module: "gleam".into(), fields: vec![field(make_variable(3), None)], }, Term::Variant { variable: subjects[2].clone(), name: "False".into(), module: "gleam".into(), fields: Vec::new(), }, ]; let mapping = get_mapping(terms); assert_eq!( printer.print_terms(subjects, terms, &mapping), "Ok(_), _, False" ); } }