aiken_lang/tipo/
fields.rs1use 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 #[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 Some(l) => (
101 l,
102 &args
103 .get(i)
104 .expect("Indexing in labelled field reordering")
105 .location,
106 ),
107
108 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 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}