//! R4(b): a complementary pair may not share an owned binding. //! //! Two sibling emissions guarded on the same predicate, one `when` and one //! `unless`, are the split form of an `if`/`else`. R9 says a guarded member's //! value is built whether or not it is placed, so both arms are evaluated, and //! a binding that lands in a field in both is moved twice. //! //! The compiler already refuses that. What it refuses is the generated code, //! and the message points at a `let` the declaration never wrote: //! //! ```text //! error[E0382]: use of moved value: `label` //! | //! 23 | cells.push(Cell::new(label)); //! | ----- value moved here //! 26 | cells.push(Cell::new(label)); //! | ^^^^^ value used here after move //! ``` //! //! So this refuses it first, against the declaration, naming the binding and //! the remedy. Measured incidence across the ratified 514 shapes is zero //! written-out pairs, which is what makes this a guard against reintroduction //! rather than a fix: nobody writes the split form today, and the trap is only //! sprung on re-authoring. Wiki `quasi-declare-form` section 11. //! //! # The remedy the message names //! //! `given`, which R7 expands to a real `match`. Arms of a match are exclusive, //! so a value moved in one is fine. Amendment 14 added `"true"` and `"false"` //! to `pattern` for exactly this case: the complementary pair R4(b) describes //! is boolean, and until that amendment the only construct that made a pair //! unnecessary could not be written for the only case that makes one dangerous. use std::collections::BTreeMap; use syn::Result; use crate::ast::{Arg, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Source, Step}; /// Refuse every complementary pair that shares an owned binding. pub fn check(items: &[Item]) -> Result<()> { let mut guarded: Vec<(&Guard, &Emission)> = Vec::new(); for item in items { match item { Item::Emit(Emission::Guarded { guard, inner }) => guarded.push((guard, inner)), Item::For { body, .. } => check(body)?, _ => {} } if let Item::Emit(emission) = item { check(bodies(emission))?; } } for (at, (guard, inner)) in guarded.iter().enumerate() { for (other, later) in guarded.iter().skip(at + 1) { if !complementary(guard, other) { continue; } let mine = moved(inner); let theirs = moved(later); if let Some(name) = mine.keys().find(|name| theirs.contains_key(*name)) { let span = mine[name]; return Err(syn::Error::new( span, format!( "`{name}` is moved by both arms of a complementary pair. A guarded \ member is built whether or not it is placed, so `when` and `unless` \ on one predicate move it twice. Say it as one `given` over the same \ predicate with `true` and `false` arms, which are exclusive." ), )); } } } Ok(()) } /// The bodies one emission holds, so the walk reaches every sibling set. fn bodies(emission: &Emission) -> &[Item] { match emission { Emission::Simple { body, .. } | Emission::Chip { body, .. } | Emission::Screen { body, .. } | Emission::Row { body, .. } | Emission::Form { body, .. } | Emission::Field { body, .. } | Emission::Act { body, .. } | Emission::Offers { body, .. } | Emission::Offering { body, .. } | Emission::Removes { body, .. } | Emission::Repeats { body, .. } | Emission::Column { body, .. } | Emission::Cell { body, .. } | Emission::Region { body, .. } | Emission::Across { body, .. } => body, Emission::List(body) | Emission::Table(body) | Emission::Cells(body) => body, Emission::Beside { inner, .. } | Emission::Framed { inner, .. } | Emission::At { inner, .. } => bodies(inner), Emission::Guarded { inner, .. } => bodies(inner), Emission::Include(_) | Emission::IncludeEach(_) | Emission::Link { .. } | Emission::Activate(_) | Emission::Given { .. } => &[], } } /// Whether these two guards are one predicate asked both ways. fn complementary(one: &Guard, other: &Guard) -> bool { one.negated != other.negated && key(one) == key(other) } /// A predicate's shape as text, for comparing two of them. /// /// Through the emitter rather than by walking the tree again, so two predicates /// compare equal exactly when they generate the same Rust. fn key(guard: &Guard) -> String { let bare = Guard { negated: false, predicate: guard.predicate.clone(), span: guard.span, }; crate::emit::predicate(&bare).map_or_else(|_| String::new(), |tokens| tokens.to_string()) } /// The bindings this emission moves, and where each is written. /// /// A hole moves its root when what it names lands somewhere by value: the root /// itself, or a field read off it. A method call does not, because it returns /// something new, and neither does a borrow, because R8 makes the `&` visible. fn moved(emission: &Emission) -> BTreeMap { let mut found = BTreeMap::new(); walk_emission(emission, &mut found); found } fn note(hole: &Hole, found: &mut BTreeMap) { let HoleRoot::Binding(name) = &hole.root else { return; }; if hole .steps .iter() .any(|step| matches!(step, Step::Method { .. })) { return; } found.entry(name.to_string()).or_insert_with(|| name.span()); } fn walk_arg(arg: &Arg, found: &mut BTreeMap) { match arg { Arg::Hole(hole) => note(hole, found), Arg::Str(text) => walk_str(text, found), Arg::List(items) => items.iter().for_each(|item| walk_arg(item, found)), // R8: the ampersand is written where the code writes it, so a borrowed // argument is visibly not a move and this rule has nothing to say. Arg::Borrow(_) => {} Arg::Int(_) | Arg::Bool(_) => {} } } /// An interpolation formats its holes and moves none of them. fn walk_str(_text: &Interpolated, _found: &mut BTreeMap) {} fn walk_items(items: &[Item], found: &mut BTreeMap) { for item in items { match item { Item::Bind { source, .. } => { if let Source::Hole(hole) = source { note(hole, found); } } Item::Attribute { args, .. } => args.iter().for_each(|arg| walk_arg(arg, found)), Item::For { body, .. } | Item::Marked { body, .. } => walk_items(body, found), Item::Emit(emission) => walk_emission(emission, found), } } } fn walk_emission(emission: &Emission, found: &mut BTreeMap) { match emission { Emission::Simple { args, .. } | Emission::Screen { args, .. } => { for arg in args { walk_arg(arg, found); } } Emission::Cell { value, .. } => walk_arg(value, found), Emission::Row { primary, .. } => walk_arg(primary, found), Emission::Column { name, .. } => walk_arg(name, found), Emission::Chip { value, .. } => walk_arg(value, found), Emission::Act { label, .. } | Emission::Offers { label, .. } | Emission::Offering { label, .. } | Emission::Removes { label, .. } => walk_arg(label, found), Emission::Include(hole) | Emission::IncludeEach(hole) => note(hole, found), Emission::Beside { inner, .. } | Emission::Framed { inner, .. } | Emission::At { inner, .. } => { walk_emission(inner, found); } Emission::Guarded { inner, .. } => walk_emission(inner, found), _ => {} } walk_items(bodies(emission), found); }