Skip to main content

max / quasi

8.0 KB · 209 lines History Blame Raw
1 //! R4(b): a complementary pair may not share an owned binding.
2 //!
3 //! Two sibling emissions guarded on the same predicate, one `when` and one
4 //! `unless`, are the split form of an `if`/`else`. R9 says a guarded member's
5 //! value is built whether or not it is placed, so both arms are evaluated, and
6 //! a binding that lands in a field in both is moved twice.
7 //!
8 //! The compiler already refuses that. What it refuses is the generated code,
9 //! and the message points at a `let` the declaration never wrote:
10 //!
11 //! ```text
12 //! error[E0382]: use of moved value: `label`
13 //! |
14 //! 23 | cells.push(Cell::new(label));
15 //! | ----- value moved here
16 //! 26 | cells.push(Cell::new(label));
17 //! | ^^^^^ value used here after move
18 //! ```
19 //!
20 //! So this refuses it first, against the declaration, naming the binding and
21 //! the remedy. Measured incidence across the ratified 514 shapes is zero
22 //! written-out pairs, which is what makes this a guard against reintroduction
23 //! rather than a fix: nobody writes the split form today, and the trap is only
24 //! sprung on re-authoring. Wiki `quasi-declare-form` section 11.
25 //!
26 //! # The remedy the message names
27 //!
28 //! `given`, which R7 expands to a real `match`. Arms of a match are exclusive,
29 //! so a value moved in one is fine. Amendment 14 added `"true"` and `"false"`
30 //! to `pattern` for exactly this case: the complementary pair R4(b) describes
31 //! is boolean, and until that amendment the only construct that made a pair
32 //! unnecessary could not be written for the only case that makes one dangerous.
33
34 use std::collections::BTreeMap;
35
36 use syn::Result;
37
38 use crate::ast::{Arg, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Source, Step};
39
40 /// Refuse every complementary pair that shares an owned binding.
41 pub fn check(items: &[Item]) -> Result<()> {
42 let mut guarded: Vec<(&Guard, &Emission)> = Vec::new();
43
44 for item in items {
45 match item {
46 Item::Emit(Emission::Guarded { guard, inner }) => guarded.push((guard, inner)),
47 Item::For { body, .. } => check(body)?,
48 _ => {}
49 }
50 if let Item::Emit(emission) = item {
51 check(bodies(emission))?;
52 }
53 }
54
55 for (at, (guard, inner)) in guarded.iter().enumerate() {
56 for (other, later) in guarded.iter().skip(at + 1) {
57 if !complementary(guard, other) {
58 continue;
59 }
60 let mine = moved(inner);
61 let theirs = moved(later);
62 if let Some(name) = mine.keys().find(|name| theirs.contains_key(*name)) {
63 let span = mine[name];
64 return Err(syn::Error::new(
65 span,
66 format!(
67 "`{name}` is moved by both arms of a complementary pair. A guarded \
68 member is built whether or not it is placed, so `when` and `unless` \
69 on one predicate move it twice. Say it as one `given` over the same \
70 predicate with `true` and `false` arms, which are exclusive."
71 ),
72 ));
73 }
74 }
75 }
76 Ok(())
77 }
78
79 /// The bodies one emission holds, so the walk reaches every sibling set.
80 fn bodies(emission: &Emission) -> &[Item] {
81 match emission {
82 Emission::Simple { body, .. }
83 | Emission::Chip { body, .. }
84 | Emission::Screen { body, .. }
85 | Emission::Row { body, .. }
86 | Emission::Form { body, .. }
87 | Emission::Field { body, .. }
88 | Emission::Act { body, .. }
89 | Emission::Offers { body, .. }
90 | Emission::Offering { body, .. }
91 | Emission::Removes { body, .. }
92 | Emission::Repeats { body, .. }
93 | Emission::Column { body, .. }
94 | Emission::Cell { body, .. }
95 | Emission::Region { body, .. }
96 | Emission::Across { body, .. } => body,
97 Emission::List(body) | Emission::Table(body) | Emission::Cells(body) => body,
98 Emission::Beside { inner, .. }
99 | Emission::Framed { inner, .. }
100 | Emission::At { inner, .. } => bodies(inner),
101 Emission::Guarded { inner, .. } => bodies(inner),
102 Emission::Include(_)
103 | Emission::IncludeEach(_)
104 | Emission::Link { .. }
105 | Emission::Activate(_)
106 | Emission::Given { .. } => &[],
107 }
108 }
109
110 /// Whether these two guards are one predicate asked both ways.
111 fn complementary(one: &Guard, other: &Guard) -> bool {
112 one.negated != other.negated && key(one) == key(other)
113 }
114
115 /// A predicate's shape as text, for comparing two of them.
116 ///
117 /// Through the emitter rather than by walking the tree again, so two predicates
118 /// compare equal exactly when they generate the same Rust.
119 fn key(guard: &Guard) -> String {
120 let bare = Guard {
121 negated: false,
122 predicate: guard.predicate.clone(),
123 span: guard.span,
124 };
125 crate::emit::predicate(&bare).map_or_else(|_| String::new(), |tokens| tokens.to_string())
126 }
127
128 /// The bindings this emission moves, and where each is written.
129 ///
130 /// A hole moves its root when what it names lands somewhere by value: the root
131 /// itself, or a field read off it. A method call does not, because it returns
132 /// something new, and neither does a borrow, because R8 makes the `&` visible.
133 fn moved(emission: &Emission) -> BTreeMap<String, proc_macro2::Span> {
134 let mut found = BTreeMap::new();
135 walk_emission(emission, &mut found);
136 found
137 }
138
139 fn note(hole: &Hole, found: &mut BTreeMap<String, proc_macro2::Span>) {
140 let HoleRoot::Binding(name) = &hole.root else {
141 return;
142 };
143 if hole
144 .steps
145 .iter()
146 .any(|step| matches!(step, Step::Method { .. }))
147 {
148 return;
149 }
150 found.entry(name.to_string()).or_insert_with(|| name.span());
151 }
152
153 fn walk_arg(arg: &Arg, found: &mut BTreeMap<String, proc_macro2::Span>) {
154 match arg {
155 Arg::Hole(hole) => note(hole, found),
156 Arg::Str(text) => walk_str(text, found),
157 Arg::List(items) => items.iter().for_each(|item| walk_arg(item, found)),
158 // R8: the ampersand is written where the code writes it, so a borrowed
159 // argument is visibly not a move and this rule has nothing to say.
160 Arg::Borrow(_) => {}
161 Arg::Int(_) | Arg::Bool(_) => {}
162 }
163 }
164
165 /// An interpolation formats its holes and moves none of them.
166 fn walk_str(_text: &Interpolated, _found: &mut BTreeMap<String, proc_macro2::Span>) {}
167
168 fn walk_items(items: &[Item], found: &mut BTreeMap<String, proc_macro2::Span>) {
169 for item in items {
170 match item {
171 Item::Bind { source, .. } => {
172 if let Source::Hole(hole) = source {
173 note(hole, found);
174 }
175 }
176 Item::Attribute { args, .. } => args.iter().for_each(|arg| walk_arg(arg, found)),
177 Item::For { body, .. } | Item::Marked { body, .. } => walk_items(body, found),
178 Item::Emit(emission) => walk_emission(emission, found),
179 }
180 }
181 }
182
183 fn walk_emission(emission: &Emission, found: &mut BTreeMap<String, proc_macro2::Span>) {
184 match emission {
185 Emission::Simple { args, .. } | Emission::Screen { args, .. } => {
186 for arg in args {
187 walk_arg(arg, found);
188 }
189 }
190 Emission::Cell { value, .. } => walk_arg(value, found),
191 Emission::Row { primary, .. } => walk_arg(primary, found),
192 Emission::Column { name, .. } => walk_arg(name, found),
193 Emission::Chip { value, .. } => walk_arg(value, found),
194 Emission::Act { label, .. }
195 | Emission::Offers { label, .. }
196 | Emission::Offering { label, .. }
197 | Emission::Removes { label, .. } => walk_arg(label, found),
198 Emission::Include(hole) | Emission::IncludeEach(hole) => note(hole, found),
199 Emission::Beside { inner, .. }
200 | Emission::Framed { inner, .. }
201 | Emission::At { inner, .. } => {
202 walk_emission(inner, found);
203 }
204 Emission::Guarded { inner, .. } => walk_emission(inner, found),
205 _ => {}
206 }
207 walk_items(bodies(emission), found);
208 }
209