Skip to content

Commit

Permalink
Rollup merge of rust-lang#78748 - fanzier:tuple-assignment, r=petroch…
Browse files Browse the repository at this point in the history
…enkov

Implement destructuring assignment for tuples

This is the first step towards implementing destructuring assignment (RFC: rust-lang/rfcs#2909, tracking issue: rust-lang#71126). This PR is the first part of rust-lang#71156, which was split up to allow for easier review.

Quick summary: This change allows destructuring the LHS of an assignment if it's a (possibly nested) tuple.
It is implemented via a desugaring (AST -> HIR lowering) as follows:
```rust
(a,b) = (1,2)
```
... becomes ...
```rust
{
  let (lhs0,lhs1) = (1,2);
  a = lhs0;
  b = lhs1;
}
```

Thanks to `@varkor` who helped with the implementation, particularly around default binding modes.

r? `@petrochenkov`
  • Loading branch information
Dylan-DPC authored Nov 9, 2020
2 parents b4589a8 + 3a7a997 commit abaa78b
Show file tree
Hide file tree
Showing 23 changed files with 395 additions and 86 deletions.
131 changes: 130 additions & 1 deletion compiler/rustc_ast_lowering/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use rustc_data_structures::thin_vec::ThinVec;
use rustc_errors::struct_span_err;
use rustc_hir as hir;
use rustc_hir::def::Res;
use rustc_session::parse::feature_err;
use rustc_span::hygiene::ForLoopLoc;
use rustc_span::source_map::{respan, DesugaringKind, Span, Spanned};
use rustc_span::symbol::{sym, Ident, Symbol};
Expand Down Expand Up @@ -146,7 +147,7 @@ impl<'hir> LoweringContext<'_, 'hir> {
hir::ExprKind::Block(self.lower_block(blk, opt_label.is_some()), opt_label)
}
ExprKind::Assign(ref el, ref er, span) => {
hir::ExprKind::Assign(self.lower_expr(el), self.lower_expr(er), span)
self.lower_expr_assign(el, er, span, e.span)
}
ExprKind::AssignOp(op, ref el, ref er) => hir::ExprKind::AssignOp(
self.lower_binop(op),
Expand Down Expand Up @@ -840,6 +841,134 @@ impl<'hir> LoweringContext<'_, 'hir> {
})
}

/// Destructure the LHS of complex assignments.
/// For instance, lower `(a, b) = t` to `{ let (lhs1, lhs2) = t; a = lhs1; b = lhs2; }`.
fn lower_expr_assign(
&mut self,
lhs: &Expr,
rhs: &Expr,
eq_sign_span: Span,
whole_span: Span,
) -> hir::ExprKind<'hir> {
// Return early in case of an ordinary assignment.
fn is_ordinary(lhs: &Expr) -> bool {
match &lhs.kind {
ExprKind::Tup(..) => false,
ExprKind::Paren(e) => {
match e.kind {
// We special-case `(..)` for consistency with patterns.
ExprKind::Range(None, None, RangeLimits::HalfOpen) => false,
_ => is_ordinary(e),
}
}
_ => true,
}
}
if is_ordinary(lhs) {
return hir::ExprKind::Assign(self.lower_expr(lhs), self.lower_expr(rhs), eq_sign_span);
}
if !self.sess.features_untracked().destructuring_assignment {
feature_err(
&self.sess.parse_sess,
sym::destructuring_assignment,
eq_sign_span,
"destructuring assignments are unstable",
)
.span_label(lhs.span, "cannot assign to this expression")
.emit();
}

let mut assignments = vec![];

// The LHS becomes a pattern: `(lhs1, lhs2)`.
let pat = self.destructure_assign(lhs, eq_sign_span, &mut assignments);
let rhs = self.lower_expr(rhs);

// Introduce a `let` for destructuring: `let (lhs1, lhs2) = t`.
let destructure_let = self.stmt_let_pat(
ThinVec::new(),
whole_span,
Some(rhs),
pat,
hir::LocalSource::AssignDesugar(eq_sign_span),
);

// `a = lhs1; b = lhs2;`.
let stmts = self
.arena
.alloc_from_iter(std::iter::once(destructure_let).chain(assignments.into_iter()));

// Wrap everything in a block.
hir::ExprKind::Block(&self.block_all(whole_span, stmts, None), None)
}

/// Convert the LHS of a destructuring assignment to a pattern.
/// Each sub-assignment is recorded in `assignments`.
fn destructure_assign(
&mut self,
lhs: &Expr,
eq_sign_span: Span,
assignments: &mut Vec<hir::Stmt<'hir>>,
) -> &'hir hir::Pat<'hir> {
match &lhs.kind {
// Tuples.
ExprKind::Tup(elements) => {
let (pats, rest) =
self.destructure_sequence(elements, "tuple", eq_sign_span, assignments);
let tuple_pat = hir::PatKind::Tuple(pats, rest.map(|r| r.0));
return self.pat_without_dbm(lhs.span, tuple_pat);
}
ExprKind::Paren(e) => {
// We special-case `(..)` for consistency with patterns.
if let ExprKind::Range(None, None, RangeLimits::HalfOpen) = e.kind {
let tuple_pat = hir::PatKind::Tuple(&[], Some(0));
return self.pat_without_dbm(lhs.span, tuple_pat);
} else {
return self.destructure_assign(e, eq_sign_span, assignments);
}
}
_ => {}
}
// Treat all other cases as normal lvalue.
let ident = Ident::new(sym::lhs, lhs.span);
let (pat, binding) = self.pat_ident(lhs.span, ident);
let ident = self.expr_ident(lhs.span, ident, binding);
let assign = hir::ExprKind::Assign(self.lower_expr(lhs), ident, eq_sign_span);
let expr = self.expr(lhs.span, assign, ThinVec::new());
assignments.push(self.stmt_expr(lhs.span, expr));
pat
}

/// Destructure a sequence of expressions occurring on the LHS of an assignment.
/// Such a sequence occurs in a tuple (struct)/slice.
/// Return a sequence of corresponding patterns, and the index and the span of `..` if it
/// exists.
/// Each sub-assignment is recorded in `assignments`.
fn destructure_sequence(
&mut self,
elements: &[AstP<Expr>],
ctx: &str,
eq_sign_span: Span,
assignments: &mut Vec<hir::Stmt<'hir>>,
) -> (&'hir [&'hir hir::Pat<'hir>], Option<(usize, Span)>) {
let mut rest = None;
let elements =
self.arena.alloc_from_iter(elements.iter().enumerate().filter_map(|(i, e)| {
// Check for `..` pattern.
if let ExprKind::Range(None, None, RangeLimits::HalfOpen) = e.kind {
if let Some((_, prev_span)) = rest {
self.ban_extra_rest_pat(e.span, prev_span, ctx);
} else {
rest = Some((i, e.span));
}
None
} else {
Some(self.destructure_assign(e, eq_sign_span, assignments))
}
}));
(elements, rest)
}

/// Desugar `<start>..=<end>` into `std::ops::RangeInclusive::new(<start>, <end>)`.
fn lower_expr_range_closed(&mut self, span: Span, e1: &Expr, e2: &Expr) -> hir::ExprKind<'hir> {
let e1 = self.lower_expr_mut(e1);
Expand Down
17 changes: 16 additions & 1 deletion compiler/rustc_ast_lowering/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
hir_id,
kind: hir::PatKind::Binding(bm, hir_id, ident.with_span_pos(span), None),
span,
default_binding_modes: true,
}),
hir_id,
)
Expand All @@ -2541,7 +2542,21 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
}

fn pat(&mut self, span: Span, kind: hir::PatKind<'hir>) -> &'hir hir::Pat<'hir> {
self.arena.alloc(hir::Pat { hir_id: self.next_id(), kind, span })
self.arena.alloc(hir::Pat {
hir_id: self.next_id(),
kind,
span,
default_binding_modes: true,
})
}

fn pat_without_dbm(&mut self, span: Span, kind: hir::PatKind<'hir>) -> &'hir hir::Pat<'hir> {
self.arena.alloc(hir::Pat {
hir_id: self.next_id(),
kind,
span,
default_binding_modes: false,
})
}

fn ty_path(
Expand Down
9 changes: 7 additions & 2 deletions compiler/rustc_ast_lowering/src/pat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,16 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {

/// Construct a `Pat` with the `HirId` of `p.id` lowered.
fn pat_with_node_id_of(&mut self, p: &Pat, kind: hir::PatKind<'hir>) -> &'hir hir::Pat<'hir> {
self.arena.alloc(hir::Pat { hir_id: self.lower_node_id(p.id), kind, span: p.span })
self.arena.alloc(hir::Pat {
hir_id: self.lower_node_id(p.id),
kind,
span: p.span,
default_binding_modes: true,
})
}

/// Emit a friendly error for extra `..` patterns in a tuple/tuple struct/slice pattern.
fn ban_extra_rest_pat(&self, sp: Span, prev_sp: Span, ctx: &str) {
crate fn ban_extra_rest_pat(&self, sp: Span, prev_sp: Span, ctx: &str) {
self.diagnostic()
.struct_span_err(sp, &format!("`..` can only be used once per {} pattern", ctx))
.span_label(sp, &format!("can only be used once per {} pattern", ctx))
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_feature/src/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,9 @@ declare_features! (
/// Allows unsized fn parameters.
(active, unsized_fn_params, "1.49.0", Some(48055), None),

/// Allows the use of destructuring assignments.
(active, destructuring_assignment, "1.49.0", Some(71126), None),

// -------------------------------------------------------------------------
// feature-group-end: actual feature gates
// -------------------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions compiler/rustc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,9 @@ pub struct Pat<'hir> {
pub hir_id: HirId,
pub kind: PatKind<'hir>,
pub span: Span,
// Whether to use default binding modes.
// At present, this is false only for destructuring assignment.
pub default_binding_modes: bool,
}

impl Pat<'_> {
Expand Down Expand Up @@ -1680,6 +1683,9 @@ pub enum LocalSource {
AsyncFn,
/// A desugared `<expr>.await`.
AwaitDesugar,
/// A desugared `expr = expr`, where the LHS is a tuple, struct or array.
/// The span is that of the `=` sign.
AssignDesugar(Span),
}

/// Hints at the original code for a `match _ { .. }`.
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_mir_build/src/thir/pattern/check_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ impl<'tcx> Visitor<'tcx> for MatchVisitor<'_, 'tcx> {
hir::LocalSource::ForLoopDesugar => ("`for` loop binding", None),
hir::LocalSource::AsyncFn => ("async fn binding", None),
hir::LocalSource::AwaitDesugar => ("`await` future binding", None),
hir::LocalSource::AssignDesugar(_) => ("destructuring assignment binding", None),
};
self.check_irrefutable(&loc.pat, msg, sp);
self.check_patterns(&loc.pat);
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ symbols! {
deref_mut,
deref_target,
derive,
destructuring_assignment,
diagnostic,
direct,
discriminant_kind,
Expand Down
37 changes: 11 additions & 26 deletions compiler/rustc_typeck/src/check/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,39 +718,24 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
);
}

fn is_destructuring_place_expr(&self, expr: &'tcx hir::Expr<'tcx>) -> bool {
match &expr.kind {
ExprKind::Array(comps) | ExprKind::Tup(comps) => {
comps.iter().all(|e| self.is_destructuring_place_expr(e))
}
ExprKind::Struct(_path, fields, rest) => {
rest.as_ref().map(|e| self.is_destructuring_place_expr(e)).unwrap_or(true)
&& fields.iter().all(|f| self.is_destructuring_place_expr(&f.expr))
}
_ => expr.is_syntactic_place_expr(),
}
}

pub(crate) fn check_lhs_assignable(
&self,
lhs: &'tcx hir::Expr<'tcx>,
err_code: &'static str,
expr_span: &Span,
) {
if !lhs.is_syntactic_place_expr() {
// FIXME: Make this use SessionDiagnostic once error codes can be dynamically set.
let mut err = self.tcx.sess.struct_span_err_with_code(
*expr_span,
"invalid left-hand side of assignment",
DiagnosticId::Error(err_code.into()),
);
err.span_label(lhs.span, "cannot assign to this expression");
if self.is_destructuring_place_expr(lhs) {
err.note("destructuring assignments are not currently supported");
err.note("for more information, see https://github.com/rust-lang/rfcs/issues/372");
}
err.emit();
if lhs.is_syntactic_place_expr() {
return;
}

// FIXME: Make this use SessionDiagnostic once error codes can be dynamically set.
let mut err = self.tcx.sess.struct_span_err_with_code(
*expr_span,
"invalid left-hand side of assignment",
DiagnosticId::Error(err_code.into()),
);
err.span_label(lhs.span, "cannot assign to this expression");
err.emit();
}

/// Type check assignment expression `expr` of form `lhs = rhs`.
Expand Down
5 changes: 5 additions & 0 deletions compiler/rustc_typeck/src/check/pat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
///
/// When the pattern is a path pattern, `opt_path_res` must be `Some(res)`.
fn calc_adjust_mode(&self, pat: &'tcx Pat<'tcx>, opt_path_res: Option<Res>) -> AdjustMode {
// When we perform destructuring assignment, we disable default match bindings, which are
// unintuitive in this context.
if !pat.default_binding_modes {
return AdjustMode::Reset;
}
match &pat.kind {
// Type checking these product-like types successfully always require
// that the expected type be of those types and not reference types.
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_typeck/src/check/regionck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ impl<'a, 'tcx> RegionCtxt<'a, 'tcx> {
fn link_pattern(&self, discr_cmt: PlaceWithHirId<'tcx>, root_pat: &hir::Pat<'_>) {
debug!("link_pattern(discr_cmt={:?}, root_pat={:?})", discr_cmt, root_pat);
ignore_err!(self.with_mc(|mc| {
mc.cat_pattern(discr_cmt, root_pat, |sub_cmt, hir::Pat { kind, span, hir_id }| {
mc.cat_pattern(discr_cmt, root_pat, |sub_cmt, hir::Pat { kind, span, hir_id, .. }| {
// `ref x` pattern
if let PatKind::Binding(..) = kind {
if let Some(ty::BindByReference(mutbl)) =
Expand Down
6 changes: 4 additions & 2 deletions src/test/ui/bad/bad-expr-lhs.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
fn main() {
1 = 2; //~ ERROR invalid left-hand side of assignment
1 += 2; //~ ERROR invalid left-hand side of assignment
(1, 2) = (3, 4); //~ ERROR invalid left-hand side of assignment
(1, 2) = (3, 4); //~ ERROR destructuring assignments are unstable
//~| ERROR invalid left-hand side of assignment
//~| ERROR invalid left-hand side of assignment

let (a, b) = (1, 2);
(a, b) = (3, 4); //~ ERROR invalid left-hand side of assignment
(a, b) = (3, 4); //~ ERROR destructuring assignments are unstable

None = Some(3); //~ ERROR invalid left-hand side of assignment
}
Loading

0 comments on commit abaa78b

Please sign in to comment.