]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_const_eval/src/transform/check_consts/ops.rs
Rollup merge of #85766 - workingjubilee:file-options, r=yaahc
[rust.git] / compiler / rustc_const_eval / src / transform / check_consts / ops.rs
1 //! Concrete error types for all operations which may be invalid in a certain const context.
2
3 use rustc_errors::{struct_span_err, DiagnosticBuilder};
4 use rustc_hir as hir;
5 use rustc_hir::def_id::DefId;
6 use rustc_middle::mir;
7 use rustc_session::parse::feature_err;
8 use rustc_span::symbol::sym;
9 use rustc_span::{Span, Symbol};
10
11 use super::ConstCx;
12
13 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14 pub enum Status {
15     Allowed,
16     Unstable(Symbol),
17     Forbidden,
18 }
19
20 #[derive(Clone, Copy)]
21 pub enum DiagnosticImportance {
22     /// An operation that must be removed for const-checking to pass.
23     Primary,
24
25     /// An operation that causes const-checking to fail, but is usually a side-effect of a `Primary` operation elsewhere.
26     Secondary,
27 }
28
29 /// An operation that is not *always* allowed in a const context.
30 pub trait NonConstOp: std::fmt::Debug {
31     /// Returns an enum indicating whether this operation is allowed within the given item.
32     fn status_in_item(&self, _ccx: &ConstCx<'_, '_>) -> Status {
33         Status::Forbidden
34     }
35
36     fn importance(&self) -> DiagnosticImportance {
37         DiagnosticImportance::Primary
38     }
39
40     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx>;
41 }
42
43 #[derive(Debug)]
44 pub struct FloatingPointOp;
45 impl NonConstOp for FloatingPointOp {
46     fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
47         if ccx.const_kind() == hir::ConstContext::ConstFn {
48             Status::Unstable(sym::const_fn_floating_point_arithmetic)
49         } else {
50             Status::Allowed
51         }
52     }
53
54     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
55         feature_err(
56             &ccx.tcx.sess.parse_sess,
57             sym::const_fn_floating_point_arithmetic,
58             span,
59             &format!("floating point arithmetic is not allowed in {}s", ccx.const_kind()),
60         )
61     }
62 }
63
64 /// A function call where the callee is a pointer.
65 #[derive(Debug)]
66 pub struct FnCallIndirect;
67 impl NonConstOp for FnCallIndirect {
68     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
69         ccx.tcx.sess.struct_span_err(span, "function pointers are not allowed in const fn")
70     }
71 }
72
73 /// A function call where the callee is not marked as `const`.
74 #[derive(Debug)]
75 pub struct FnCallNonConst;
76 impl NonConstOp for FnCallNonConst {
77     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
78         struct_span_err!(
79             ccx.tcx.sess,
80             span,
81             E0015,
82             "calls in {}s are limited to constant functions, \
83              tuple structs and tuple variants",
84             ccx.const_kind(),
85         )
86     }
87 }
88
89 /// A call to an `#[unstable]` const fn or `#[rustc_const_unstable]` function.
90 ///
91 /// Contains the name of the feature that would allow the use of this function.
92 #[derive(Debug)]
93 pub struct FnCallUnstable(pub DefId, pub Option<Symbol>);
94
95 impl NonConstOp for FnCallUnstable {
96     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
97         let FnCallUnstable(def_id, feature) = *self;
98
99         let mut err = ccx.tcx.sess.struct_span_err(
100             span,
101             &format!("`{}` is not yet stable as a const fn", ccx.tcx.def_path_str(def_id)),
102         );
103
104         if ccx.is_const_stable_const_fn() {
105             err.help("const-stable functions can only call other const-stable functions");
106         } else if ccx.tcx.sess.is_nightly_build() {
107             if let Some(feature) = feature {
108                 err.help(&format!(
109                     "add `#![feature({})]` to the crate attributes to enable",
110                     feature
111                 ));
112             }
113         }
114
115         err
116     }
117 }
118
119 #[derive(Debug)]
120 pub struct FnPtrCast;
121 impl NonConstOp for FnPtrCast {
122     fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
123         if ccx.const_kind() != hir::ConstContext::ConstFn {
124             Status::Allowed
125         } else {
126             Status::Unstable(sym::const_fn_fn_ptr_basics)
127         }
128     }
129
130     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
131         feature_err(
132             &ccx.tcx.sess.parse_sess,
133             sym::const_fn_fn_ptr_basics,
134             span,
135             &format!("function pointer casts are not allowed in {}s", ccx.const_kind()),
136         )
137     }
138 }
139
140 #[derive(Debug)]
141 pub struct Generator(pub hir::GeneratorKind);
142 impl NonConstOp for Generator {
143     fn status_in_item(&self, _: &ConstCx<'_, '_>) -> Status {
144         if let hir::GeneratorKind::Async(hir::AsyncGeneratorKind::Block) = self.0 {
145             Status::Unstable(sym::const_async_blocks)
146         } else {
147             Status::Forbidden
148         }
149     }
150
151     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
152         let msg = format!("{}s are not allowed in {}s", self.0, ccx.const_kind());
153         if let hir::GeneratorKind::Async(hir::AsyncGeneratorKind::Block) = self.0 {
154             feature_err(&ccx.tcx.sess.parse_sess, sym::const_async_blocks, span, &msg)
155         } else {
156             ccx.tcx.sess.struct_span_err(span, &msg)
157         }
158     }
159 }
160
161 #[derive(Debug)]
162 pub struct HeapAllocation;
163 impl NonConstOp for HeapAllocation {
164     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
165         let mut err = struct_span_err!(
166             ccx.tcx.sess,
167             span,
168             E0010,
169             "allocations are not allowed in {}s",
170             ccx.const_kind()
171         );
172         err.span_label(span, format!("allocation not allowed in {}s", ccx.const_kind()));
173         if ccx.tcx.sess.teach(&err.get_code().unwrap()) {
174             err.note(
175                 "The value of statics and constants must be known at compile time, \
176                  and they live for the entire lifetime of a program. Creating a boxed \
177                  value allocates memory on the heap at runtime, and therefore cannot \
178                  be done at compile time.",
179             );
180         }
181         err
182     }
183 }
184
185 #[derive(Debug)]
186 pub struct InlineAsm;
187 impl NonConstOp for InlineAsm {
188     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
189         struct_span_err!(
190             ccx.tcx.sess,
191             span,
192             E0015,
193             "inline assembly is not allowed in {}s",
194             ccx.const_kind()
195         )
196     }
197 }
198
199 #[derive(Debug)]
200 pub struct LiveDrop {
201     pub dropped_at: Option<Span>,
202 }
203 impl NonConstOp for LiveDrop {
204     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
205         let mut err = struct_span_err!(
206             ccx.tcx.sess,
207             span,
208             E0493,
209             "destructors cannot be evaluated at compile-time"
210         );
211         err.span_label(span, format!("{}s cannot evaluate destructors", ccx.const_kind()));
212         if let Some(span) = self.dropped_at {
213             err.span_label(span, "value is dropped here");
214         }
215         err
216     }
217 }
218
219 #[derive(Debug)]
220 /// A borrow of a type that contains an `UnsafeCell` somewhere. The borrow never escapes to
221 /// the final value of the constant.
222 pub struct TransientCellBorrow;
223 impl NonConstOp for TransientCellBorrow {
224     fn status_in_item(&self, _: &ConstCx<'_, '_>) -> Status {
225         Status::Unstable(sym::const_refs_to_cell)
226     }
227     fn importance(&self) -> DiagnosticImportance {
228         // The cases that cannot possibly work will already emit a `CellBorrow`, so we should
229         // not additionally emit a feature gate error if activating the feature gate won't work.
230         DiagnosticImportance::Secondary
231     }
232     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
233         feature_err(
234             &ccx.tcx.sess.parse_sess,
235             sym::const_refs_to_cell,
236             span,
237             "cannot borrow here, since the borrowed element may contain interior mutability",
238         )
239     }
240 }
241
242 #[derive(Debug)]
243 /// A borrow of a type that contains an `UnsafeCell` somewhere. The borrow might escape to
244 /// the final value of the constant, and thus we cannot allow this (for now). We may allow
245 /// it in the future for static items.
246 pub struct CellBorrow;
247 impl NonConstOp for CellBorrow {
248     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
249         let mut err = struct_span_err!(
250             ccx.tcx.sess,
251             span,
252             E0492,
253             "{}s cannot refer to interior mutable data",
254             ccx.const_kind(),
255         );
256         err.span_label(
257             span,
258             "this borrow of an interior mutable value may end up in the final value",
259         );
260         if let hir::ConstContext::Static(_) = ccx.const_kind() {
261             err.help(
262                 "to fix this, the value can be extracted to a separate \
263                 `static` item and then referenced",
264             );
265         }
266         if ccx.tcx.sess.teach(&err.get_code().unwrap()) {
267             err.note(
268                 "A constant containing interior mutable data behind a reference can allow you
269                  to modify that data. This would make multiple uses of a constant to be able to
270                  see different values and allow circumventing the `Send` and `Sync` requirements
271                  for shared mutable data, which is unsound.",
272             );
273         }
274         err
275     }
276 }
277
278 #[derive(Debug)]
279 /// This op is for `&mut` borrows in the trailing expression of a constant
280 /// which uses the "enclosing scopes rule" to leak its locals into anonymous
281 /// static or const items.
282 pub struct MutBorrow(pub hir::BorrowKind);
283
284 impl NonConstOp for MutBorrow {
285     fn status_in_item(&self, _ccx: &ConstCx<'_, '_>) -> Status {
286         Status::Forbidden
287     }
288
289     fn importance(&self) -> DiagnosticImportance {
290         // If there were primary errors (like non-const function calls), do not emit further
291         // errors about mutable references.
292         DiagnosticImportance::Secondary
293     }
294
295     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
296         let raw = match self.0 {
297             hir::BorrowKind::Raw => "raw ",
298             hir::BorrowKind::Ref => "",
299         };
300
301         let mut err = struct_span_err!(
302             ccx.tcx.sess,
303             span,
304             E0764,
305             "{}mutable references are not allowed in the final value of {}s",
306             raw,
307             ccx.const_kind(),
308         );
309
310         if ccx.tcx.sess.teach(&err.get_code().unwrap()) {
311             err.note(
312                 "References in statics and constants may only refer \
313                       to immutable values.\n\n\
314                       Statics are shared everywhere, and if they refer to \
315                       mutable data one might violate memory safety since \
316                       holding multiple mutable references to shared data \
317                       is not allowed.\n\n\
318                       If you really want global mutable state, try using \
319                       static mut or a global UnsafeCell.",
320             );
321         }
322         err
323     }
324 }
325
326 #[derive(Debug)]
327 pub struct TransientMutBorrow(pub hir::BorrowKind);
328
329 impl NonConstOp for TransientMutBorrow {
330     fn status_in_item(&self, _: &ConstCx<'_, '_>) -> Status {
331         Status::Unstable(sym::const_mut_refs)
332     }
333
334     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
335         let raw = match self.0 {
336             hir::BorrowKind::Raw => "raw ",
337             hir::BorrowKind::Ref => "",
338         };
339
340         feature_err(
341             &ccx.tcx.sess.parse_sess,
342             sym::const_mut_refs,
343             span,
344             &format!("{}mutable references are not allowed in {}s", raw, ccx.const_kind()),
345         )
346     }
347 }
348
349 #[derive(Debug)]
350 pub struct MutDeref;
351 impl NonConstOp for MutDeref {
352     fn status_in_item(&self, _: &ConstCx<'_, '_>) -> Status {
353         Status::Unstable(sym::const_mut_refs)
354     }
355
356     fn importance(&self) -> DiagnosticImportance {
357         // Usually a side-effect of a `TransientMutBorrow` somewhere.
358         DiagnosticImportance::Secondary
359     }
360
361     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
362         feature_err(
363             &ccx.tcx.sess.parse_sess,
364             sym::const_mut_refs,
365             span,
366             &format!("mutation through a reference is not allowed in {}s", ccx.const_kind()),
367         )
368     }
369 }
370
371 /// A call to a `panic()` lang item where the first argument is _not_ a `&str`.
372 #[derive(Debug)]
373 pub struct PanicNonStr;
374 impl NonConstOp for PanicNonStr {
375     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
376         ccx.tcx.sess.struct_span_err(
377             span,
378             "argument to `panic!()` in a const context must have type `&str`",
379         )
380     }
381 }
382
383 /// Comparing raw pointers for equality.
384 /// Not currently intended to ever be allowed, even behind a feature gate: operation depends on
385 /// allocation base addresses that are not known at compile-time.
386 #[derive(Debug)]
387 pub struct RawPtrComparison;
388 impl NonConstOp for RawPtrComparison {
389     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
390         let mut err = ccx
391             .tcx
392             .sess
393             .struct_span_err(span, "pointers cannot be reliably compared during const eval");
394         err.note(
395             "see issue #53020 <https://github.com/rust-lang/rust/issues/53020> \
396             for more information",
397         );
398         err
399     }
400 }
401
402 #[derive(Debug)]
403 pub struct RawMutPtrDeref;
404 impl NonConstOp for RawMutPtrDeref {
405     fn status_in_item(&self, _: &ConstCx<'_, '_>) -> Status {
406         Status::Unstable(sym::const_mut_refs)
407     }
408
409     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
410         feature_err(
411             &ccx.tcx.sess.parse_sess,
412             sym::const_mut_refs,
413             span,
414             &format!("dereferencing raw mutable pointers in {}s is unstable", ccx.const_kind(),),
415         )
416     }
417 }
418
419 /// Casting raw pointer or function pointer to an integer.
420 /// Not currently intended to ever be allowed, even behind a feature gate: operation depends on
421 /// allocation base addresses that are not known at compile-time.
422 #[derive(Debug)]
423 pub struct RawPtrToIntCast;
424 impl NonConstOp for RawPtrToIntCast {
425     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
426         let mut err = ccx
427             .tcx
428             .sess
429             .struct_span_err(span, "pointers cannot be cast to integers during const eval");
430         err.note("at compile-time, pointers do not have an integer value");
431         err.note(
432             "avoiding this restriction via `transmute`, `union`, or raw pointers leads to compile-time undefined behavior",
433         );
434         err
435     }
436 }
437
438 /// An access to a (non-thread-local) `static`.
439 #[derive(Debug)]
440 pub struct StaticAccess;
441 impl NonConstOp for StaticAccess {
442     fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
443         if let hir::ConstContext::Static(_) = ccx.const_kind() {
444             Status::Allowed
445         } else {
446             Status::Forbidden
447         }
448     }
449
450     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
451         let mut err = struct_span_err!(
452             ccx.tcx.sess,
453             span,
454             E0013,
455             "{}s cannot refer to statics",
456             ccx.const_kind()
457         );
458         err.help(
459             "consider extracting the value of the `static` to a `const`, and referring to that",
460         );
461         if ccx.tcx.sess.teach(&err.get_code().unwrap()) {
462             err.note(
463                 "`static` and `const` variables can refer to other `const` variables. \
464                     A `const` variable, however, cannot refer to a `static` variable.",
465             );
466             err.help("To fix this, the value can be extracted to a `const` and then used.");
467         }
468         err
469     }
470 }
471
472 /// An access to a thread-local `static`.
473 #[derive(Debug)]
474 pub struct ThreadLocalAccess;
475 impl NonConstOp for ThreadLocalAccess {
476     fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
477         struct_span_err!(
478             ccx.tcx.sess,
479             span,
480             E0625,
481             "thread-local statics cannot be \
482             accessed at compile-time"
483         )
484     }
485 }
486
487 // Types that cannot appear in the signature or locals of a `const fn`.
488 pub mod ty {
489     use super::*;
490
491     #[derive(Debug)]
492     pub struct MutRef(pub mir::LocalKind);
493     impl NonConstOp for MutRef {
494         fn status_in_item(&self, _ccx: &ConstCx<'_, '_>) -> Status {
495             Status::Unstable(sym::const_mut_refs)
496         }
497
498         fn importance(&self) -> DiagnosticImportance {
499             match self.0 {
500                 mir::LocalKind::Var | mir::LocalKind::Temp => DiagnosticImportance::Secondary,
501                 mir::LocalKind::ReturnPointer | mir::LocalKind::Arg => {
502                     DiagnosticImportance::Primary
503                 }
504             }
505         }
506
507         fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
508             feature_err(
509                 &ccx.tcx.sess.parse_sess,
510                 sym::const_mut_refs,
511                 span,
512                 &format!("mutable references are not allowed in {}s", ccx.const_kind()),
513             )
514         }
515     }
516
517     #[derive(Debug)]
518     pub struct FnPtr(pub mir::LocalKind);
519     impl NonConstOp for FnPtr {
520         fn importance(&self) -> DiagnosticImportance {
521             match self.0 {
522                 mir::LocalKind::Var | mir::LocalKind::Temp => DiagnosticImportance::Secondary,
523                 mir::LocalKind::ReturnPointer | mir::LocalKind::Arg => {
524                     DiagnosticImportance::Primary
525                 }
526             }
527         }
528
529         fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
530             if ccx.const_kind() != hir::ConstContext::ConstFn {
531                 Status::Allowed
532             } else {
533                 Status::Unstable(sym::const_fn_fn_ptr_basics)
534             }
535         }
536
537         fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
538             feature_err(
539                 &ccx.tcx.sess.parse_sess,
540                 sym::const_fn_fn_ptr_basics,
541                 span,
542                 &format!("function pointers cannot appear in {}s", ccx.const_kind()),
543             )
544         }
545     }
546
547     #[derive(Debug)]
548     pub struct ImplTrait;
549     impl NonConstOp for ImplTrait {
550         fn status_in_item(&self, _: &ConstCx<'_, '_>) -> Status {
551             Status::Unstable(sym::const_impl_trait)
552         }
553
554         fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
555             feature_err(
556                 &ccx.tcx.sess.parse_sess,
557                 sym::const_impl_trait,
558                 span,
559                 &format!("`impl Trait` is not allowed in {}s", ccx.const_kind()),
560             )
561         }
562     }
563
564     #[derive(Debug)]
565     pub struct TraitBound(pub mir::LocalKind);
566     impl NonConstOp for TraitBound {
567         fn importance(&self) -> DiagnosticImportance {
568             match self.0 {
569                 mir::LocalKind::Var | mir::LocalKind::Temp => DiagnosticImportance::Secondary,
570                 mir::LocalKind::ReturnPointer | mir::LocalKind::Arg => {
571                     DiagnosticImportance::Primary
572                 }
573             }
574         }
575
576         fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
577             if ccx.const_kind() != hir::ConstContext::ConstFn {
578                 Status::Allowed
579             } else {
580                 Status::Unstable(sym::const_fn_trait_bound)
581             }
582         }
583
584         fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
585             let mut err = feature_err(
586                 &ccx.tcx.sess.parse_sess,
587                 sym::const_fn_trait_bound,
588                 span,
589                 "trait bounds other than `Sized` on const fn parameters are unstable",
590             );
591
592             match ccx.fn_sig() {
593                 Some(fn_sig) if !fn_sig.span.contains(span) => {
594                     err.span_label(fn_sig.span, "function declared as const here");
595                 }
596                 _ => {}
597             }
598
599             err
600         }
601     }
602
603     #[derive(Debug)]
604     pub struct DynTrait(pub mir::LocalKind);
605     impl NonConstOp for DynTrait {
606         fn importance(&self) -> DiagnosticImportance {
607             match self.0 {
608                 mir::LocalKind::Var | mir::LocalKind::Temp => DiagnosticImportance::Secondary,
609                 mir::LocalKind::ReturnPointer | mir::LocalKind::Arg => {
610                     DiagnosticImportance::Primary
611                 }
612             }
613         }
614
615         fn status_in_item(&self, ccx: &ConstCx<'_, '_>) -> Status {
616             if ccx.const_kind() != hir::ConstContext::ConstFn {
617                 Status::Allowed
618             } else {
619                 Status::Unstable(sym::const_fn_trait_bound)
620             }
621         }
622
623         fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
624             let mut err = feature_err(
625                 &ccx.tcx.sess.parse_sess,
626                 sym::const_fn_trait_bound,
627                 span,
628                 "trait objects in const fn are unstable",
629             );
630
631             match ccx.fn_sig() {
632                 Some(fn_sig) if !fn_sig.span.contains(span) => {
633                     err.span_label(fn_sig.span, "function declared as const here");
634                 }
635                 _ => {}
636             }
637
638             err
639         }
640     }
641
642     /// A trait bound with the `?const Trait` opt-out
643     #[derive(Debug)]
644     pub struct TraitBoundNotConst;
645     impl NonConstOp for TraitBoundNotConst {
646         fn status_in_item(&self, _: &ConstCx<'_, '_>) -> Status {
647             Status::Unstable(sym::const_trait_bound_opt_out)
648         }
649
650         fn build_error(&self, ccx: &ConstCx<'_, 'tcx>, span: Span) -> DiagnosticBuilder<'tcx> {
651             feature_err(
652                 &ccx.tcx.sess.parse_sess,
653                 sym::const_trait_bound_opt_out,
654                 span,
655                 "`?const Trait` syntax is unstable",
656             )
657         }
658     }
659 }