hir_ty/diagnostics/
expr.rs

1//! Various diagnostics for expressions that are collected together in one pass
2//! through the body using inference results: mismatched arg counts, missing
3//! fields, etc.
4
5use std::fmt;
6
7use base_db::Crate;
8use either::Either;
9use hir_def::{
10    AdtId, AssocItemId, DefWithBodyId, HasModule, ItemContainerId, Lookup,
11    lang_item::LangItems,
12    resolver::{HasResolver, ValueNs},
13};
14use intern::sym;
15use itertools::Itertools;
16use rustc_hash::FxHashSet;
17use rustc_pattern_analysis::constructor::Constructor;
18use rustc_type_ir::inherent::{AdtDef, IntoKind};
19use syntax::{
20    AstNode,
21    ast::{self, UnaryOp},
22};
23use tracing::debug;
24use triomphe::Arc;
25use typed_arena::Arena;
26
27use crate::{
28    Adjust, InferenceResult,
29    db::HirDatabase,
30    diagnostics::match_check::{
31        self,
32        pat_analysis::{self, DeconstructedPat, MatchCheckCtx, WitnessPat},
33    },
34    display::{DisplayTarget, HirDisplay},
35    next_solver::{
36        DbInterner, ParamEnv, Ty, TyKind, TypingMode,
37        infer::{DbInternerInferExt, InferCtxt},
38    },
39};
40
41pub(crate) use hir_def::{
42    LocalFieldId, VariantId,
43    expr_store::Body,
44    hir::{Expr, ExprId, MatchArm, Pat, PatId, Statement},
45};
46
47pub enum BodyValidationDiagnostic {
48    RecordMissingFields {
49        record: Either<ExprId, PatId>,
50        variant: VariantId,
51        missed_fields: Vec<LocalFieldId>,
52    },
53    ReplaceFilterMapNextWithFindMap {
54        method_call_expr: ExprId,
55    },
56    MissingMatchArms {
57        match_expr: ExprId,
58        uncovered_patterns: String,
59    },
60    NonExhaustiveLet {
61        pat: PatId,
62        uncovered_patterns: String,
63    },
64    RemoveTrailingReturn {
65        return_expr: ExprId,
66    },
67    RemoveUnnecessaryElse {
68        if_expr: ExprId,
69    },
70}
71
72impl BodyValidationDiagnostic {
73    pub fn collect(
74        db: &dyn HirDatabase,
75        owner: DefWithBodyId,
76        validate_lints: bool,
77    ) -> Vec<BodyValidationDiagnostic> {
78        let _p = tracing::info_span!("BodyValidationDiagnostic::collect").entered();
79        let infer = InferenceResult::for_body(db, owner);
80        let body = db.body(owner);
81        let env = db.trait_environment_for_body(owner);
82        let interner = DbInterner::new_with(db, owner.krate(db));
83        let infcx =
84            interner.infer_ctxt().build(TypingMode::typeck_for_body(interner, owner.into()));
85        let mut validator = ExprValidator {
86            owner,
87            body,
88            infer,
89            diagnostics: Vec::new(),
90            validate_lints,
91            env,
92            infcx,
93        };
94        validator.validate_body();
95        validator.diagnostics
96    }
97}
98
99struct ExprValidator<'db> {
100    owner: DefWithBodyId,
101    body: Arc<Body>,
102    infer: &'db InferenceResult<'db>,
103    env: ParamEnv<'db>,
104    diagnostics: Vec<BodyValidationDiagnostic>,
105    validate_lints: bool,
106    infcx: InferCtxt<'db>,
107}
108
109impl<'db> ExprValidator<'db> {
110    #[inline]
111    fn db(&self) -> &'db dyn HirDatabase {
112        self.infcx.interner.db
113    }
114
115    fn validate_body(&mut self) {
116        let db = self.db();
117        let mut filter_map_next_checker = None;
118        // we'll pass &mut self while iterating over body.exprs, so they need to be disjoint
119        let body = Arc::clone(&self.body);
120
121        if matches!(self.owner, DefWithBodyId::FunctionId(_)) {
122            self.check_for_trailing_return(body.body_expr, &body);
123        }
124
125        for (id, expr) in body.exprs() {
126            if let Some((variant, missed_fields, true)) =
127                record_literal_missing_fields(db, self.infer, id, expr)
128            {
129                self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
130                    record: Either::Left(id),
131                    variant,
132                    missed_fields,
133                });
134            }
135
136            match expr {
137                Expr::Match { expr, arms } => {
138                    self.validate_match(id, *expr, arms);
139                }
140                Expr::Call { .. } | Expr::MethodCall { .. } => {
141                    self.validate_call(id, expr, &mut filter_map_next_checker);
142                }
143                Expr::Closure { body: body_expr, .. } => {
144                    self.check_for_trailing_return(*body_expr, &body);
145                }
146                Expr::If { .. } => {
147                    self.check_for_unnecessary_else(id, expr);
148                }
149                Expr::Block { .. } | Expr::Async { .. } | Expr::Unsafe { .. } => {
150                    self.validate_block(expr);
151                }
152                _ => {}
153            }
154        }
155
156        for (id, pat) in body.pats() {
157            if let Some((variant, missed_fields, true)) =
158                record_pattern_missing_fields(db, self.infer, id, pat)
159            {
160                self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
161                    record: Either::Right(id),
162                    variant,
163                    missed_fields,
164                });
165            }
166        }
167    }
168
169    fn validate_call(
170        &mut self,
171        call_id: ExprId,
172        expr: &Expr,
173        filter_map_next_checker: &mut Option<FilterMapNextChecker<'db>>,
174    ) {
175        if !self.validate_lints {
176            return;
177        }
178        // Check that the number of arguments matches the number of parameters.
179
180        if self.infer.expr_type_mismatches().next().is_some() {
181            // FIXME: Due to shortcomings in the current type system implementation, only emit
182            // this diagnostic if there are no type mismatches in the containing function.
183        } else if let Expr::MethodCall { receiver, .. } = expr {
184            let (callee, _) = match self.infer.method_resolution(call_id) {
185                Some(it) => it,
186                None => return,
187            };
188
189            let checker = filter_map_next_checker.get_or_insert_with(|| {
190                FilterMapNextChecker::new(self.infcx.interner.lang_items(), self.db())
191            });
192
193            if checker.check(call_id, receiver, &callee).is_some() {
194                self.diagnostics.push(BodyValidationDiagnostic::ReplaceFilterMapNextWithFindMap {
195                    method_call_expr: call_id,
196                });
197            }
198
199            if let Some(receiver_ty) = self.infer.type_of_expr_with_adjust(*receiver) {
200                checker.prev_receiver_ty = Some(receiver_ty);
201            }
202        }
203    }
204
205    fn validate_match(&mut self, match_expr: ExprId, scrutinee_expr: ExprId, arms: &[MatchArm]) {
206        let Some(scrut_ty) = self.infer.type_of_expr_with_adjust(scrutinee_expr) else {
207            return;
208        };
209        if scrut_ty.references_non_lt_error() {
210            return;
211        }
212
213        let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env);
214
215        let pattern_arena = Arena::new();
216        let mut m_arms = Vec::with_capacity(arms.len());
217        let mut has_lowering_errors = false;
218        // Note: Skipping the entire diagnostic rather than just not including a faulty match arm is
219        // preferred to avoid the chance of false positives.
220        for arm in arms {
221            let Some(pat_ty) = self.infer.type_of_pat_with_adjust(arm.pat) else {
222                return;
223            };
224            if pat_ty.references_non_lt_error() {
225                return;
226            }
227
228            // We only include patterns whose type matches the type
229            // of the scrutinee expression. If we had an InvalidMatchArmPattern
230            // diagnostic or similar we could raise that in an else
231            // block here.
232            //
233            // When comparing the types, we also have to consider that rustc
234            // will automatically de-reference the scrutinee expression type if
235            // necessary.
236            //
237            // FIXME we should use the type checker for this.
238            if (pat_ty == scrut_ty
239                || scrut_ty
240                    .as_reference()
241                    .map(|(match_expr_ty, ..)| match_expr_ty == pat_ty)
242                    .unwrap_or(false))
243                && types_of_subpatterns_do_match(arm.pat, &self.body, self.infer)
244            {
245                // If we had a NotUsefulMatchArm diagnostic, we could
246                // check the usefulness of each pattern as we added it
247                // to the matrix here.
248                let pat = self.lower_pattern(&cx, arm.pat, &mut has_lowering_errors);
249                let m_arm = pat_analysis::MatchArm {
250                    pat: pattern_arena.alloc(pat),
251                    has_guard: arm.guard.is_some(),
252                    arm_data: (),
253                };
254                m_arms.push(m_arm);
255                if !has_lowering_errors {
256                    continue;
257                }
258            }
259            // If the pattern type doesn't fit the match expression, we skip this diagnostic.
260            cov_mark::hit!(validate_match_bailed_out);
261            return;
262        }
263
264        let known_valid_scrutinee = Some(self.is_known_valid_scrutinee(scrutinee_expr));
265        let report =
266            match cx.compute_match_usefulness(m_arms.as_slice(), scrut_ty, known_valid_scrutinee) {
267                Ok(report) => report,
268                Err(()) => return,
269            };
270
271        // FIXME Report unreachable arms
272        // https://github.com/rust-lang/rust/blob/f31622a50/compiler/rustc_mir_build/src/thir/pattern/check_match.rs#L200
273
274        let witnesses = report.non_exhaustiveness_witnesses;
275        if !witnesses.is_empty() {
276            self.diagnostics.push(BodyValidationDiagnostic::MissingMatchArms {
277                match_expr,
278                uncovered_patterns: missing_match_arms(
279                    &cx,
280                    scrut_ty,
281                    witnesses,
282                    m_arms.is_empty(),
283                    self.owner.krate(self.db()),
284                ),
285            });
286        }
287    }
288
289    // [rustc's `is_known_valid_scrutinee`](https://github.com/rust-lang/rust/blob/c9bd03cb724e13cca96ad320733046cbdb16fbbe/compiler/rustc_mir_build/src/thir/pattern/check_match.rs#L288)
290    //
291    // While the above function in rustc uses thir exprs, r-a doesn't have them.
292    // So, the logic here is getting same result as "hir lowering + match with lowered thir"
293    // with "hir only"
294    fn is_known_valid_scrutinee(&self, scrutinee_expr: ExprId) -> bool {
295        let db = self.db();
296
297        if self
298            .infer
299            .expr_adjustments
300            .get(&scrutinee_expr)
301            .is_some_and(|adjusts| adjusts.iter().any(|a| matches!(a.kind, Adjust::Deref(..))))
302        {
303            return false;
304        }
305
306        match &self.body[scrutinee_expr] {
307            Expr::UnaryOp { op: UnaryOp::Deref, .. } => false,
308            Expr::Path(path) => {
309                let value_or_partial = self.owner.resolver(db).resolve_path_in_value_ns_fully(
310                    db,
311                    path,
312                    self.body.expr_path_hygiene(scrutinee_expr),
313                );
314                value_or_partial.is_none_or(|v| !matches!(v, ValueNs::StaticId(_)))
315            }
316            Expr::Field { expr, .. } => match self.infer.type_of_expr[*expr].kind() {
317                TyKind::Adt(adt, ..) if matches!(adt.def_id().0, AdtId::UnionId(_)) => false,
318                _ => self.is_known_valid_scrutinee(*expr),
319            },
320            Expr::Index { base, .. } => self.is_known_valid_scrutinee(*base),
321            Expr::Cast { expr, .. } => self.is_known_valid_scrutinee(*expr),
322            Expr::Missing => false,
323            _ => true,
324        }
325    }
326
327    fn validate_block(&mut self, expr: &Expr) {
328        let (Expr::Block { statements, .. }
329        | Expr::Async { statements, .. }
330        | Expr::Unsafe { statements, .. }) = expr
331        else {
332            return;
333        };
334        let pattern_arena = Arena::new();
335        let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env);
336        for stmt in &**statements {
337            let &Statement::Let { pat, initializer, else_branch: None, .. } = stmt else {
338                continue;
339            };
340            if self.infer.type_mismatch_for_pat(pat).is_some() {
341                continue;
342            }
343            let Some(initializer) = initializer else { continue };
344            let Some(ty) = self.infer.type_of_expr_with_adjust(initializer) else { continue };
345            if ty.references_non_lt_error() {
346                continue;
347            }
348
349            let mut have_errors = false;
350            let deconstructed_pat = self.lower_pattern(&cx, pat, &mut have_errors);
351
352            // optimization, wildcard trivially hold
353            if have_errors || matches!(deconstructed_pat.ctor(), Constructor::Wildcard) {
354                continue;
355            }
356
357            let match_arm = rustc_pattern_analysis::MatchArm {
358                pat: pattern_arena.alloc(deconstructed_pat),
359                has_guard: false,
360                arm_data: (),
361            };
362            let report = match cx.compute_match_usefulness(&[match_arm], ty, None) {
363                Ok(v) => v,
364                Err(e) => {
365                    debug!(?e, "match usefulness error");
366                    continue;
367                }
368            };
369            let witnesses = report.non_exhaustiveness_witnesses;
370            if !witnesses.is_empty() {
371                self.diagnostics.push(BodyValidationDiagnostic::NonExhaustiveLet {
372                    pat,
373                    uncovered_patterns: missing_match_arms(
374                        &cx,
375                        ty,
376                        witnesses,
377                        false,
378                        self.owner.krate(self.db()),
379                    ),
380                });
381            }
382        }
383    }
384
385    fn lower_pattern<'a>(
386        &self,
387        cx: &MatchCheckCtx<'a, 'db>,
388        pat: PatId,
389        have_errors: &mut bool,
390    ) -> DeconstructedPat<'a, 'db> {
391        let mut patcx = match_check::PatCtxt::new(self.db(), self.infer, &self.body);
392        let pattern = patcx.lower_pattern(pat);
393        let pattern = cx.lower_pat(&pattern);
394        if !patcx.errors.is_empty() {
395            *have_errors = true;
396        }
397        pattern
398    }
399
400    fn check_for_trailing_return(&mut self, body_expr: ExprId, body: &Body) {
401        if !self.validate_lints {
402            return;
403        }
404        match &body[body_expr] {
405            Expr::Block { statements, tail, .. } => {
406                let last_stmt = tail.or_else(|| match statements.last()? {
407                    Statement::Expr { expr, .. } => Some(*expr),
408                    _ => None,
409                });
410                if let Some(last_stmt) = last_stmt {
411                    self.check_for_trailing_return(last_stmt, body);
412                }
413            }
414            Expr::If { then_branch, else_branch, .. } => {
415                self.check_for_trailing_return(*then_branch, body);
416                if let Some(else_branch) = else_branch {
417                    self.check_for_trailing_return(*else_branch, body);
418                }
419            }
420            Expr::Match { arms, .. } => {
421                for arm in arms.iter() {
422                    let MatchArm { expr, .. } = arm;
423                    self.check_for_trailing_return(*expr, body);
424                }
425            }
426            Expr::Return { .. } => {
427                self.diagnostics.push(BodyValidationDiagnostic::RemoveTrailingReturn {
428                    return_expr: body_expr,
429                });
430            }
431            _ => (),
432        }
433    }
434
435    fn check_for_unnecessary_else(&mut self, id: ExprId, expr: &Expr) {
436        if !self.validate_lints {
437            return;
438        }
439        if let Expr::If { condition: _, then_branch, else_branch } = expr {
440            if else_branch.is_none() {
441                return;
442            }
443            if let Expr::Block { statements, tail, .. } = &self.body[*then_branch] {
444                let last_then_expr = tail.or_else(|| match statements.last()? {
445                    Statement::Expr { expr, .. } => Some(*expr),
446                    _ => None,
447                });
448                if let Some(last_then_expr) = last_then_expr
449                    && let Some(last_then_expr_ty) =
450                        self.infer.type_of_expr_with_adjust(last_then_expr)
451                    && last_then_expr_ty.is_never()
452                {
453                    // Only look at sources if the then branch diverges and we have an else branch.
454                    let source_map = self.db().body_with_source_map(self.owner).1;
455                    let Ok(source_ptr) = source_map.expr_syntax(id) else {
456                        return;
457                    };
458                    let root = source_ptr.file_syntax(self.db());
459                    let either::Left(ast::Expr::IfExpr(if_expr)) = source_ptr.value.to_node(&root)
460                    else {
461                        return;
462                    };
463                    let mut top_if_expr = if_expr;
464                    loop {
465                        let parent = top_if_expr.syntax().parent();
466                        let has_parent_expr_stmt_or_stmt_list =
467                            parent.as_ref().is_some_and(|node| {
468                                ast::ExprStmt::can_cast(node.kind())
469                                    | ast::StmtList::can_cast(node.kind())
470                            });
471                        if has_parent_expr_stmt_or_stmt_list {
472                            // Only emit diagnostic if parent or direct ancestor is either
473                            // an expr stmt or a stmt list.
474                            break;
475                        }
476                        let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else {
477                            // Bail if parent is neither an if expr, an expr stmt nor a stmt list.
478                            return;
479                        };
480                        // Check parent if expr.
481                        top_if_expr = parent_if_expr;
482                    }
483
484                    self.diagnostics
485                        .push(BodyValidationDiagnostic::RemoveUnnecessaryElse { if_expr: id })
486                }
487            }
488        }
489    }
490}
491
492struct FilterMapNextChecker<'db> {
493    filter_map_function_id: Option<hir_def::FunctionId>,
494    next_function_id: Option<hir_def::FunctionId>,
495    prev_filter_map_expr_id: Option<ExprId>,
496    prev_receiver_ty: Option<Ty<'db>>,
497}
498
499impl<'db> FilterMapNextChecker<'db> {
500    fn new(lang_items: &'db LangItems, db: &'db dyn HirDatabase) -> Self {
501        // Find and store the FunctionIds for Iterator::filter_map and Iterator::next
502        let (next_function_id, filter_map_function_id) = match lang_items.IteratorNext {
503            Some(next_function_id) => (
504                Some(next_function_id),
505                match next_function_id.lookup(db).container {
506                    ItemContainerId::TraitId(iterator_trait_id) => {
507                        let iterator_trait_items = &iterator_trait_id.trait_items(db).items;
508                        iterator_trait_items.iter().find_map(|(name, it)| match it {
509                            &AssocItemId::FunctionId(id) if *name == sym::filter_map => Some(id),
510                            _ => None,
511                        })
512                    }
513                    _ => None,
514                },
515            ),
516            None => (None, None),
517        };
518        Self {
519            filter_map_function_id,
520            next_function_id,
521            prev_filter_map_expr_id: None,
522            prev_receiver_ty: None,
523        }
524    }
525
526    // check for instances of .filter_map(..).next()
527    fn check(
528        &mut self,
529        current_expr_id: ExprId,
530        receiver_expr_id: &ExprId,
531        function_id: &hir_def::FunctionId,
532    ) -> Option<()> {
533        if *function_id == self.filter_map_function_id? {
534            self.prev_filter_map_expr_id = Some(current_expr_id);
535            return None;
536        }
537
538        if *function_id == self.next_function_id?
539            && let Some(prev_filter_map_expr_id) = self.prev_filter_map_expr_id
540        {
541            let is_dyn_trait = self
542                .prev_receiver_ty
543                .as_ref()
544                .is_some_and(|it| it.strip_references().dyn_trait().is_some());
545            if *receiver_expr_id == prev_filter_map_expr_id && !is_dyn_trait {
546                return Some(());
547            }
548        }
549
550        self.prev_filter_map_expr_id = None;
551        None
552    }
553}
554
555pub fn record_literal_missing_fields(
556    db: &dyn HirDatabase,
557    infer: &InferenceResult<'_>,
558    id: ExprId,
559    expr: &Expr,
560) -> Option<(VariantId, Vec<LocalFieldId>, /*exhaustive*/ bool)> {
561    let (fields, exhaustive) = match expr {
562        Expr::RecordLit { fields, spread, .. } => (fields, spread.is_none()),
563        _ => return None,
564    };
565
566    let variant_def = infer.variant_resolution_for_expr(id)?;
567    if let VariantId::UnionId(_) = variant_def {
568        return None;
569    }
570
571    let variant_data = variant_def.fields(db);
572
573    let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
574    let missed_fields: Vec<LocalFieldId> = variant_data
575        .fields()
576        .iter()
577        .filter_map(|(f, d)| if specified_fields.contains(&d.name) { None } else { Some(f) })
578        .collect();
579    if missed_fields.is_empty() {
580        return None;
581    }
582    Some((variant_def, missed_fields, exhaustive))
583}
584
585pub fn record_pattern_missing_fields(
586    db: &dyn HirDatabase,
587    infer: &InferenceResult<'_>,
588    id: PatId,
589    pat: &Pat,
590) -> Option<(VariantId, Vec<LocalFieldId>, /*exhaustive*/ bool)> {
591    let (fields, exhaustive) = match pat {
592        Pat::Record { path: _, args, ellipsis } => (args, !ellipsis),
593        _ => return None,
594    };
595
596    let variant_def = infer.variant_resolution_for_pat(id)?;
597    if let VariantId::UnionId(_) = variant_def {
598        return None;
599    }
600
601    let variant_data = variant_def.fields(db);
602
603    let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
604    let missed_fields: Vec<LocalFieldId> = variant_data
605        .fields()
606        .iter()
607        .filter_map(|(f, d)| if specified_fields.contains(&d.name) { None } else { Some(f) })
608        .collect();
609    if missed_fields.is_empty() {
610        return None;
611    }
612    Some((variant_def, missed_fields, exhaustive))
613}
614
615fn types_of_subpatterns_do_match(pat: PatId, body: &Body, infer: &InferenceResult<'_>) -> bool {
616    fn walk(pat: PatId, body: &Body, infer: &InferenceResult<'_>, has_type_mismatches: &mut bool) {
617        match infer.type_mismatch_for_pat(pat) {
618            Some(_) => *has_type_mismatches = true,
619            None if *has_type_mismatches => (),
620            None => {
621                let pat = &body[pat];
622                if let Pat::ConstBlock(expr) | Pat::Lit(expr) = *pat {
623                    *has_type_mismatches |= infer.type_mismatch_for_expr(expr).is_some();
624                    if *has_type_mismatches {
625                        return;
626                    }
627                }
628                pat.walk_child_pats(|subpat| walk(subpat, body, infer, has_type_mismatches))
629            }
630        }
631    }
632
633    let mut has_type_mismatches = false;
634    walk(pat, body, infer, &mut has_type_mismatches);
635    !has_type_mismatches
636}
637
638fn missing_match_arms<'a, 'db>(
639    cx: &MatchCheckCtx<'a, 'db>,
640    scrut_ty: Ty<'a>,
641    witnesses: Vec<WitnessPat<'a, 'db>>,
642    arms_is_empty: bool,
643    krate: Crate,
644) -> String {
645    struct DisplayWitness<'a, 'b, 'db>(
646        &'a WitnessPat<'b, 'db>,
647        &'a MatchCheckCtx<'b, 'db>,
648        DisplayTarget,
649    );
650    impl fmt::Display for DisplayWitness<'_, '_, '_> {
651        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652            let DisplayWitness(witness, cx, display_target) = *self;
653            let pat = cx.hoist_witness_pat(witness);
654            write!(f, "{}", pat.display(cx.db, display_target))
655        }
656    }
657
658    let non_empty_enum = match scrut_ty.as_adt() {
659        Some((AdtId::EnumId(e), _)) => !e.enum_variants(cx.db).variants.is_empty(),
660        _ => false,
661    };
662    let display_target = DisplayTarget::from_crate(cx.db, krate);
663    if arms_is_empty && !non_empty_enum {
664        format!("type `{}` is non-empty", scrut_ty.display(cx.db, display_target))
665    } else {
666        let pat_display = |witness| DisplayWitness(witness, cx, display_target);
667        const LIMIT: usize = 3;
668        match &*witnesses {
669            [witness] => format!("`{}` not covered", pat_display(witness)),
670            [head @ .., tail] if head.len() < LIMIT => {
671                let head = head.iter().map(pat_display);
672                format!("`{}` and `{}` not covered", head.format("`, `"), pat_display(tail))
673            }
674            _ => {
675                let (head, tail) = witnesses.split_at(LIMIT);
676                let head = head.iter().map(pat_display);
677                format!("`{}` and {} more not covered", head.format("`, `"), tail.len())
678            }
679        }
680    }
681}