Skip to main content

aiken_lang/tipo/
fields.rs

1use super::error::{Error, UnknownLabels};
2use crate::ast::{CallArg, Span};
3use itertools::Itertools;
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7pub struct FieldMap {
8    pub arity: usize,
9    pub fields: HashMap<String, (usize, Span)>,
10    pub is_function: bool,
11}
12
13impl FieldMap {
14    pub fn new(arity: usize, is_function: bool) -> Self {
15        Self {
16            arity,
17            fields: HashMap::new(),
18            is_function,
19        }
20    }
21
22    #[allow(clippy::result_large_err)]
23    pub fn insert(&mut self, label: String, index: usize, location: &Span) -> Result<(), Error> {
24        match self.fields.insert(label.clone(), (index, *location)) {
25            Some((_, location_other)) => {
26                if self.is_function {
27                    Err(Error::DuplicateArgument {
28                        label,
29                        location: *location,
30                        duplicate_location: location_other,
31                    })
32                } else {
33                    Err(Error::DuplicateField {
34                        label,
35                        location: *location,
36                        duplicate_location: location_other,
37                    })
38                }
39            }
40            None => Ok(()),
41        }
42    }
43
44    pub fn into_option(self) -> Option<Self> {
45        if self.fields.is_empty() {
46            None
47        } else {
48            Some(self)
49        }
50    }
51
52    /// Reorder an argument list so that labelled fields supplied out-of-order are
53    /// in the correct order.
54    #[allow(clippy::result_large_err)]
55    pub fn reorder<A>(&self, args: &mut [CallArg<A>], location: Span) -> Result<(), Error> {
56        let mut last_labeled_arguments_given: Option<&CallArg<A>> = None;
57        let mut seen_labels = std::collections::HashSet::new();
58        let mut unknown_labels = Vec::new();
59
60        if self.arity != args.len() {
61            return Err(Error::IncorrectFieldsArity {
62                location,
63                expected: self.arity,
64                given: args.len(),
65            });
66        }
67
68        let mut positional_args_after_labeled = Vec::new();
69
70        for arg in args.iter() {
71            match &arg.label {
72                Some(_) => {
73                    last_labeled_arguments_given = Some(arg);
74                }
75                None => {
76                    if let Some(label) = last_labeled_arguments_given {
77                        positional_args_after_labeled.push((arg.location, label.location))
78                    }
79                }
80            }
81        }
82
83        if positional_args_after_labeled.len() > 1 {
84            let (location, labeled_arg_location) = positional_args_after_labeled
85                .first()
86                .expect("more than one positional args");
87
88            return Err(Error::PositionalArgumentAfterLabeled {
89                location: *location,
90                labeled_arg_location: *labeled_arg_location,
91            });
92        }
93
94        let mut i = 0;
95        while i < args.len() {
96            let label = &args.get(i).expect("Field indexing to get label").label;
97
98            let (label, &location) = match label {
99                // A labelled argument, we may need to reposition it in the array vector
100                Some(l) => (
101                    l,
102                    &args
103                        .get(i)
104                        .expect("Indexing in labelled field reordering")
105                        .location,
106                ),
107
108                // Not a labelled argument
109                None => {
110                    i += 1;
111                    continue;
112                }
113            };
114
115            let (position, duplicate_location) = match self.fields.get(label) {
116                None => {
117                    unknown_labels.push(location);
118                    i += 1;
119                    continue;
120                }
121                Some(&p) => p,
122            };
123
124            // If the argument is already in the right place
125            if position == i {
126                seen_labels.insert(label.clone());
127                i += 1;
128            } else {
129                if seen_labels.contains(label) {
130                    return Err(Error::DuplicateArgument {
131                        location,
132                        duplicate_location,
133                        label: label.to_string(),
134                    });
135                }
136
137                seen_labels.insert(label.clone());
138
139                args.swap(position, i);
140            }
141        }
142
143        if unknown_labels.is_empty() {
144            Ok(())
145        } else {
146            let valid = self.fields.keys().map(|t| t.to_string()).sorted().collect();
147
148            Err(Error::UnknownLabels(vec![UnknownLabels {
149                valid,
150                unknown: unknown_labels,
151                supplied: seen_labels.into_iter().collect(),
152            }]))
153        }
154    }
155
156    pub fn incorrect_arity_labels<A>(&self, args: &[CallArg<A>]) -> Vec<String> {
157        let given: HashSet<_> = args.iter().filter_map(|arg| arg.label.as_ref()).collect();
158
159        self.fields
160            .keys()
161            .filter(|f| !given.contains(f))
162            .sorted()
163            .cloned()
164            .collect()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::FieldMap;
171    use crate::tipo::{Span, fields::CallArg};
172    use proptest::prelude::*;
173    use std::collections::{HashMap, HashSet};
174
175    fn any_field() -> impl Strategy<Value = String> {
176        proptest::string::string_regex("[a-zA-Z]+").unwrap()
177    }
178
179    prop_compose! {
180        fn any_field_map()(
181            fields in proptest::collection::vec(any_field(), 2..5),
182            is_function in any::<bool>(),
183        ) -> FieldMap {
184            let fields = fields
185                .into_iter()
186                .collect::<HashSet<_>>()
187                .into_iter()
188                .enumerate()
189                .map(|(ix, field)| (field, (ix, Span::empty())))
190                .collect::<HashMap<_, _>>();
191
192            FieldMap {
193                arity: fields.len(),
194                fields,
195                is_function,
196            }
197        }
198    }
199
200    proptest! {
201        #[test]
202        fn reorder_never_fails_with_only_one_positional(
203            field_map in any_field_map(),
204            positional_arg_index in any::<usize>(),
205        ) {
206            let positional_arg_index = positional_arg_index % field_map.fields.len();
207
208            let mut call_args = field_map.fields.keys().cloned().enumerate().map(|(index, label)| {
209                CallArg {
210                    label: if index == positional_arg_index { None } else { Some(label) },
211                    location: Span::empty(),
212                    value: (),
213                }
214            }).collect::<Vec<_>>();
215
216            assert!(field_map.reorder(&mut call_args[..], Span::empty()).is_ok());
217
218            for (actual_index, arg) in call_args.iter().enumerate() {
219                if let Some(label) = &arg.label {
220                    let (expected_index, _) = field_map.fields.get(label).unwrap();
221                    assert_eq!(&actual_index, expected_index);
222                }
223            }
224        }
225    }
226}