Skip to main content

deno_lint/control_flow/
mod.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3#[cfg(test)]
4mod analyze_test;
5
6use deno_ast::swc::ast::*;
7use deno_ast::swc::common::SyntaxContext;
8use deno_ast::swc::utils::ExprCtx;
9use deno_ast::swc::{
10  ecma_visit::{noop_visit_type, Visit, VisitWith},
11  utils::{ExprExt, Value},
12};
13use deno_ast::view;
14use deno_ast::SourcePos;
15use deno_ast::SourceRangedForSpanned;
16use std::{
17  collections::{BTreeMap, HashSet},
18  mem::take,
19};
20
21#[derive(Debug, Clone)]
22pub struct ControlFlow {
23  meta: BTreeMap<SourcePos, Metadata>,
24}
25
26impl ControlFlow {
27  pub fn analyze(
28    program: view::Program,
29    unresolved_ctxt: SyntaxContext,
30  ) -> Self {
31    let mut v = Analyzer {
32      scope: Scope::new(None, BlockKind::Program),
33      info: Default::default(),
34      expr_ctxt: ExprCtx {
35        unresolved_ctxt,
36        is_unresolved_ref_safe: false,
37        in_strict: true,
38        remaining_depth: 4, // this is the swc default
39      },
40    };
41    match program {
42      view::Program::Module(module) => module.inner.visit_with(&mut v),
43      view::Program::Script(script) => script.inner.visit_with(&mut v),
44    }
45    ControlFlow { meta: v.info }
46  }
47
48  /// start_pos can be extracted from range of
49  ///
50  /// - All statements (including stmt.range())
51  /// - [SwitchCase]
52  pub fn meta(&self, start_pos: SourcePos) -> Option<&Metadata> {
53    self.meta.get(&start_pos)
54  }
55}
56
57/// Kind of a basic block.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum BlockKind {
60  /// Program (module or script)
61  Program,
62  /// Function's body
63  Function,
64  /// Switch case
65  Case,
66  /// If's body
67  If,
68  /// Body of a loop
69  Loop,
70  Label(Id),
71  /// Catch clause's body
72  Catch,
73  /// Finally's body
74  Finally,
75}
76
77#[derive(Debug, Default, Clone, PartialEq, Eq)]
78pub struct Metadata {
79  pub unreachable: bool,
80  end: Option<End>,
81}
82
83impl Metadata {
84  /// Returns true if a node prevents further execution.
85  pub fn stops_execution(&self) -> bool {
86    self
87      .end
88      .is_some_and(|d| matches!(d, End::Forced { .. } | End::Break))
89  }
90
91  /// Returns true if a node doesn't prevent further execution.
92  pub fn continues_execution(&self) -> bool {
93    self.end.is_none_or(|d| d == End::Continue)
94  }
95}
96
97#[derive(Debug)]
98struct Analyzer<'a> {
99  scope: Scope<'a>,
100  info: BTreeMap<SourcePos, Metadata>,
101  expr_ctxt: ExprCtx,
102}
103
104#[derive(Debug)]
105struct Scope<'a> {
106  _parent: Option<&'a Scope<'a>>,
107  /// This field exists to handle code like
108  ///
109  /// `function foo() { return bar(); function bar() { return 1; } }`
110  used_hoistable_ids: HashSet<Id>,
111
112  _kind: BlockKind,
113
114  /// Unconditionally ends with return, throw
115  end: Option<End>,
116
117  may_throw: bool,
118
119  ///
120  /// - None: Not found
121  /// - Some(None): Stopped at a break statement without label
122  /// - Some(Somd(id)): Stopped at a break statement with label id
123  found_break: Option<Option<Id>>,
124  found_continue: bool,
125}
126
127#[derive(Debug, Copy, Clone, PartialEq, Eq)]
128enum End {
129  /// Contains something that stops execution at that point.
130  /// This is represented as product of three elements (ret, throw, infinite_loop)
131  /// because sometimes these conditions are satisfied _simultaneously_.
132  /// See the example below:
133  ///
134  /// ```typescript
135  /// switch (foo) {
136  ///   case 1:
137  ///     return 1;
138  ///   case 2:
139  ///     throw 2;
140  ///   default:
141  ///     return 0;
142  /// }
143  /// ```
144  ///
145  /// In this case, the control flow can enter any one branch, which can be interpreted as
146  /// `End::Forced { ret: true, throw: true, infinite_loop: false }`.
147  Forced {
148    /// Unconditionally return
149    ret: bool,
150    /// Unconditionally throw
151    throw: bool,
152    /// Unconditionally entering infinite loop
153    infinite_loop: bool,
154  },
155
156  /// Break or continue
157  Break,
158
159  /// Pass through a block, like a function's block statement which ends without returning a value
160  /// or throwing an exception. Note that a node marked as `End::Continue` won't prevent further execution, which is
161  /// different from `End::Forced` or `End::Break`.
162  Continue,
163}
164
165impl End {
166  fn forced_return() -> Self {
167    End::Forced {
168      ret: true,
169      throw: false,
170      infinite_loop: false,
171    }
172  }
173
174  fn forced_throw() -> Self {
175    End::Forced {
176      ret: false,
177      throw: true,
178      infinite_loop: false,
179    }
180  }
181
182  fn forced_infinite_loop() -> Self {
183    End::Forced {
184      ret: false,
185      throw: false,
186      infinite_loop: true,
187    }
188  }
189
190  fn merge_forced(self, other: Self) -> Option<Self> {
191    match (self, other) {
192      (
193        End::Forced {
194          ret: r1,
195          throw: t1,
196          infinite_loop: i1,
197        },
198        End::Forced {
199          ret: r2,
200          throw: t2,
201          infinite_loop: i2,
202        },
203      ) => Some(End::Forced {
204        ret: r1 || r2,
205        throw: t1 || t2,
206        infinite_loop: i1 || i2,
207      }),
208      _ => None,
209    }
210  }
211
212  fn is_forced(&self) -> bool {
213    matches!(self, End::Forced { .. })
214  }
215}
216
217impl<'a> Scope<'a> {
218  pub fn new(parent: Option<&'a Scope<'a>>, kind: BlockKind) -> Self {
219    Self {
220      _parent: parent,
221      _kind: kind,
222      used_hoistable_ids: Default::default(),
223      end: None,
224      may_throw: false,
225      found_break: None,
226      found_continue: false,
227    }
228  }
229}
230
231impl Analyzer<'_> {
232  /// `lo` is marked as end if child scope is unconditionally finished
233  pub(super) fn with_child_scope<F>(
234    &mut self,
235    kind: BlockKind,
236    start_pos: SourcePos,
237    op: F,
238  ) where
239    F: for<'any> FnOnce(&mut Analyzer<'any>),
240  {
241    let prev_end = self.scope.end;
242    let (info, end, hoist, found_break, found_continue, may_throw) = {
243      let mut child = Analyzer {
244        info: take(&mut self.info),
245        scope: Scope::new(Some(&self.scope), kind.clone()),
246        expr_ctxt: self.expr_ctxt,
247      };
248      match kind {
249        BlockKind::Function => {}
250        _ => match prev_end {
251          Some(e) if matches!(e, End::Forced { .. }) => {
252            child.scope.end = Some(e)
253          }
254          _ => {}
255        },
256      }
257
258      op(&mut child);
259
260      (
261        take(&mut child.info),
262        child.scope.end,
263        child.scope.used_hoistable_ids,
264        child.scope.found_break,
265        child.scope.found_continue,
266        child.scope.may_throw,
267      )
268    };
269
270    self.info = info;
271    self.scope.used_hoistable_ids.extend(hoist);
272
273    // Preserve information about visited ast nodes.
274    self.scope.may_throw |= may_throw;
275
276    self.scope.found_continue |= found_continue;
277
278    match kind {
279      BlockKind::Case => {}
280      BlockKind::Function => {}
281      BlockKind::Loop => {}
282      _ => {
283        if self.scope.found_break.is_none() {
284          self.scope.found_break = found_break;
285        }
286      }
287    };
288
289    if let Some(end) = end {
290      match kind {
291        BlockKind::Program => {}
292        BlockKind::Function => {
293          match end {
294            End::Forced { .. } | End::Continue => {
295              self.mark_as_end(start_pos, end)
296            }
297            _ => { /* valid code is supposed to be unreachable here */ }
298          }
299          self.scope.end = prev_end;
300        }
301        BlockKind::Case => {}
302        BlockKind::If => {}
303        BlockKind::Loop => match end {
304          End::Break | End::Continue => {
305            self.mark_as_end(start_pos, end);
306            self.scope.end = prev_end;
307          }
308          e => {
309            self.mark_as_end(start_pos, e);
310            self.scope.end = Some(e);
311          }
312        },
313        BlockKind::Label(label) => {
314          if let Some(Some(id)) = &self.scope.found_break {
315            if *id == label {
316              // Eat break statement
317              self.scope.found_break = None;
318            }
319          }
320        }
321        BlockKind::Catch => {
322          self.mark_as_end(start_pos, end);
323        }
324        BlockKind::Finally => {
325          self.mark_as_end(start_pos, end);
326        }
327      }
328    }
329  }
330
331  fn get_end_reason(&self, start: SourcePos) -> Option<End> {
332    self.info.get(&start).and_then(|md| md.end)
333  }
334
335  /// Mark a statement as finisher - finishes execution - and expose it.
336  fn mark_as_end(&mut self, start: SourcePos, end: End) {
337    let new_end = match self.scope.end {
338      // `End::Continue` doesn't mean much about execution status, just indicating that execution has
339      // not yet stopped so far. So if `End::Forced` or `End::Break` comes and the current
340      // `self.scope.end` is `Some(End::Continue)`, then `self.scope.end` should be replaced with the
341      // coming value.
342      None | Some(End::Continue) => {
343        self.scope.end = Some(end);
344        Some(end)
345      }
346      Some(End::Break) => Some(end),
347      Some(e) => e.merge_forced(end).or(self.scope.end),
348    };
349
350    self.info.entry(start).or_default().end = new_end;
351  }
352
353  /// Visits statement or block. This method handles break and continue.
354  ///
355  /// This cannot be done in visit_stmt of Visit because
356  ///  this operation is very opinionated.
357  fn visit_stmt_or_block(&mut self, s: &Stmt) {
358    s.visit_with(self);
359
360    // break, continue **may** make execution end
361    match s {
362      Stmt::Break(..) | Stmt::Continue(..) => {
363        self.mark_as_end(s.start(), End::Break)
364      }
365      _ => {}
366    }
367  }
368}
369
370impl Visit for Analyzer<'_> {
371  noop_visit_type!();
372
373  fn visit_return_stmt(&mut self, n: &ReturnStmt) {
374    n.visit_children_with(self);
375    self.mark_as_end(n.start(), End::forced_return());
376  }
377
378  fn visit_throw_stmt(&mut self, n: &ThrowStmt) {
379    n.visit_children_with(self);
380    self.mark_as_end(n.start(), End::forced_throw());
381  }
382
383  fn visit_break_stmt(&mut self, n: &BreakStmt) {
384    if let Some(label) = &n.label {
385      let label = label.to_id();
386      self.scope.found_break = Some(Some(label));
387    } else {
388      self.scope.found_break = Some(None);
389    }
390  }
391
392  fn visit_continue_stmt(&mut self, _: &ContinueStmt) {
393    self.scope.found_continue = true;
394  }
395
396  fn visit_block_stmt(&mut self, s: &BlockStmt) {
397    s.visit_children_with(self);
398
399    if let Some(end) = self.scope.end {
400      self.mark_as_end(s.start(), end);
401    } else {
402      self.mark_as_end(s.start(), End::Continue);
403    }
404  }
405
406  fn visit_stmts(&mut self, stmts: &[Stmt]) {
407    for stmt in stmts {
408      self.visit_stmt_or_block(stmt);
409    }
410  }
411
412  fn visit_expr(&mut self, n: &Expr) {
413    n.visit_children_with(self);
414
415    if matches!(self.scope.end, None | Some(End::Continue)) {
416      match n {
417        Expr::Ident(i) => {
418          self.scope.used_hoistable_ids.insert(i.to_id());
419        }
420        Expr::This(..) => {}
421        _ => {
422          self.scope.may_throw = true;
423        }
424      }
425    }
426  }
427
428  fn visit_member_expr(&mut self, n: &MemberExpr) {
429    n.obj.visit_with(self);
430    if let MemberProp::Computed(computed_prop) = &n.prop {
431      computed_prop.visit_with(self);
432    }
433  }
434
435  fn visit_arrow_expr(&mut self, n: &ArrowExpr) {
436    self.with_child_scope(BlockKind::Function, n.start(), |a| {
437      n.visit_children_with(a);
438    })
439  }
440
441  fn visit_function(&mut self, n: &Function) {
442    self.with_child_scope(BlockKind::Function, n.start(), |a| {
443      n.visit_children_with(a);
444    })
445  }
446
447  fn visit_catch_clause(&mut self, n: &CatchClause) {
448    self.with_child_scope(BlockKind::Catch, n.start(), |a| {
449      n.visit_children_with(a);
450    });
451  }
452
453  fn visit_constructor(&mut self, n: &Constructor) {
454    self.with_child_scope(BlockKind::Function, n.start(), |a| {
455      n.visit_children_with(a);
456    });
457  }
458
459  fn visit_getter_prop(&mut self, n: &GetterProp) {
460    self.with_child_scope(BlockKind::Function, n.start(), |a| {
461      n.visit_children_with(a);
462    })
463  }
464
465  fn visit_setter_prop(&mut self, n: &SetterProp) {
466    self.with_child_scope(BlockKind::Function, n.start(), |a| {
467      n.visit_children_with(a);
468    })
469  }
470
471  fn visit_switch_stmt(&mut self, n: &SwitchStmt) {
472    let prev_end = self.scope.end;
473    n.visit_children_with(self);
474
475    let end = {
476      let has_default = n.cases.iter().any(|case| case.test.is_none());
477      let forced_end = n
478        .cases
479        .iter()
480        .filter_map(|case| self.get_end_reason(case.start()))
481        .try_fold(
482          End::Forced {
483            ret: false,
484            throw: false,
485            infinite_loop: false,
486          },
487          |acc, cur| acc.merge_forced(cur),
488        );
489
490      match forced_end {
491        Some(e) if has_default => e,
492        _ => End::Continue,
493      }
494    };
495
496    self.mark_as_end(n.start(), end);
497
498    if !matches!(end, End::Forced { .. }) {
499      self.scope.end = prev_end;
500    }
501  }
502
503  fn visit_switch_case(&mut self, n: &SwitchCase) {
504    let prev_end = self.scope.end;
505    let mut case_end = None;
506
507    self.with_child_scope(BlockKind::Case, n.start(), |a| {
508      n.cons.visit_with(a);
509
510      if a.scope.found_break.is_some() {
511        case_end = Some(End::Break);
512      } else if matches!(a.scope.end, Some(End::Forced { .. })) {
513        case_end = a.scope.end;
514      }
515    });
516
517    if let Some(end) = case_end {
518      self.mark_as_end(n.start(), end);
519    } else {
520      self.mark_as_end(n.start(), End::Continue);
521    }
522
523    self.scope.end = prev_end;
524  }
525
526  fn visit_if_stmt(&mut self, n: &IfStmt) {
527    n.test.visit_with(self);
528
529    let prev_end = self.scope.end;
530
531    self.with_child_scope(BlockKind::If, n.cons.start(), |a| {
532      a.visit_stmt_or_block(&n.cons);
533    });
534
535    let cons_reason = self.get_end_reason(n.cons.start());
536
537    match &n.alt {
538      Some(alt) => {
539        self.with_child_scope(BlockKind::If, alt.start(), |a| {
540          a.visit_stmt_or_block(alt);
541        });
542        let alt_reason = self.get_end_reason(alt.start());
543
544        match (cons_reason, alt_reason) {
545          (Some(x), Some(y)) if x.is_forced() && y.is_forced() => {
546            // This `unwrap` is safe; `x` and `y` are surely `Some(End::Forced { .. })`
547            let end = x.merge_forced(y).unwrap();
548            self.mark_as_end(n.start(), end);
549          }
550          (Some(End::Break), Some(End::Break))
551          | (Some(End::Forced { .. }), Some(End::Break))
552          | (Some(End::Break), Some(End::Forced { .. })) => {
553            self.mark_as_end(n.start(), End::Break);
554          }
555          // TODO: Check for continue
556          _ => {
557            self.mark_as_end(n.start(), End::Continue);
558          }
559        }
560      }
561      None => {
562        self.mark_as_end(n.start(), End::Continue);
563        self.scope.end = prev_end;
564      }
565    }
566  }
567
568  fn visit_stmt(&mut self, n: &Stmt) {
569    let scope_end = self
570      .scope
571      .end
572      .is_some_and(|d| matches!(d, End::Forced { .. } | End::Break));
573
574    let unreachable = if scope_end {
575      // Although execution is ended, we should handle hoisting.
576      match n {
577        Stmt::Empty(..) => false,
578        Stmt::Decl(Decl::Fn(FnDecl { ident, .. }))
579          if self.scope.used_hoistable_ids.contains(&ident.to_id()) =>
580        {
581          false
582        }
583        Stmt::Decl(Decl::Var(decl))
584          if decl.kind == VarDeclKind::Var
585            && decl.decls.iter().all(|decl| decl.init.is_none()) =>
586        {
587          false
588        }
589        // It's unreachable
590        _ => true,
591      }
592    } else {
593      false
594    };
595
596    self.info.entry(n.start()).or_default().unreachable = unreachable;
597
598    n.visit_children_with(self);
599  }
600
601  // loops
602
603  fn visit_for_stmt(&mut self, n: &ForStmt) {
604    n.init.visit_with(self);
605    n.update.visit_with(self);
606    n.test.visit_with(self);
607
608    let mut forced_end = None;
609    let expr_ctxt = self.expr_ctxt;
610
611    self.with_child_scope(BlockKind::Loop, n.body.start(), |a| {
612      n.body.visit_with(a);
613
614      let has_break = matches!(a.scope.found_break, Some(None));
615
616      if !has_break {
617        let end = match a.get_end_reason(n.body.start()) {
618          Some(e) if e.is_forced() => e,
619          _ => End::forced_infinite_loop(),
620        };
621        match &n.test {
622          None => {
623            a.mark_as_end(n.start(), end);
624            forced_end = Some(end);
625          }
626          Some(test) => {
627            if matches!(test.cast_to_bool(expr_ctxt), (_, Value::Known(true))) {
628              a.mark_as_end(n.start(), end);
629              forced_end = Some(end);
630            }
631          }
632        }
633      }
634
635      if forced_end.is_none() || has_break {
636        a.mark_as_end(n.body.start(), End::Continue);
637        a.scope.end = Some(End::Continue);
638      }
639    });
640  }
641
642  fn visit_for_of_stmt(&mut self, n: &ForOfStmt) {
643    let body_lo = n.body.start();
644
645    n.right.visit_with(self);
646
647    self.with_child_scope(BlockKind::Loop, body_lo, |a| {
648      n.body.visit_with(a);
649
650      // it's impossible to decide whether it enters loop block unconditionally, so we always mark
651      // it as `End::Continue`.
652      a.mark_as_end(body_lo, End::Continue);
653      a.scope.end = Some(End::Continue);
654    });
655  }
656
657  fn visit_for_in_stmt(&mut self, n: &ForInStmt) {
658    let body_lo = n.body.start();
659
660    n.right.visit_with(self);
661
662    self.with_child_scope(BlockKind::Loop, body_lo, |a| {
663      n.body.visit_with(a);
664
665      // it's impossible to decide whether it enters loop block unconditionally, so we always mark
666      // it as `End::Continue`.
667      a.mark_as_end(body_lo, End::Continue);
668      a.scope.end = Some(End::Continue);
669    });
670  }
671
672  fn visit_while_stmt(&mut self, n: &WhileStmt) {
673    let body_lo = n.body.start();
674    let expr_ctxt = self.expr_ctxt;
675
676    self.with_child_scope(BlockKind::Loop, body_lo, |a| {
677      n.body.visit_with(a);
678
679      let unconditionally_enter =
680        matches!(n.test.cast_to_bool(expr_ctxt), (_, Value::Known(true)));
681      let end_reason = a.get_end_reason(body_lo);
682      let return_or_throw = end_reason.is_some_and(|e| e.is_forced());
683      let has_break = matches!(a.scope.found_break, Some(None));
684
685      if unconditionally_enter && return_or_throw && !has_break {
686        // This `unwrap` is safe;
687        // if `return_or_throw` is true, `end_reason` is surely wrapped in `Some`.
688        a.mark_as_end(body_lo, end_reason.unwrap());
689        a.scope.end = end_reason;
690      } else if unconditionally_enter && !has_break {
691        let end = End::forced_infinite_loop();
692        a.mark_as_end(body_lo, end);
693        a.scope.end = Some(end);
694      } else {
695        a.mark_as_end(body_lo, End::Continue);
696        a.scope.end = Some(End::Continue);
697      }
698    });
699
700    n.test.visit_with(self);
701  }
702
703  fn visit_do_while_stmt(&mut self, n: &DoWhileStmt) {
704    let body_lo = n.body.start();
705    let expr_ctxt = self.expr_ctxt;
706
707    self.with_child_scope(BlockKind::Loop, body_lo, |a| {
708      n.body.visit_with(a);
709
710      let end_reason = a.get_end_reason(body_lo);
711      let return_or_throw = end_reason.is_some_and(|e| e.is_forced());
712      let infinite_loop =
713        matches!(n.test.cast_to_bool(expr_ctxt), (_, Value::Known(true)))
714          && a.scope.found_break.is_none();
715      let has_break = matches!(a.scope.found_break, Some(None));
716
717      if return_or_throw && !has_break {
718        // This `unwrap` is safe;
719        // if `return_or_throw` is true, `end_reason` is surely wrapped in `Some`.
720        a.mark_as_end(body_lo, end_reason.unwrap());
721        a.scope.end = end_reason;
722      } else if infinite_loop {
723        let end = End::forced_infinite_loop();
724        a.mark_as_end(body_lo, end);
725        a.scope.end = Some(end);
726      } else {
727        a.mark_as_end(body_lo, End::Continue);
728        a.scope.end = Some(End::Continue);
729      }
730    });
731
732    match self.get_end_reason(body_lo) {
733      Some(e) if e.is_forced() => {
734        self.mark_as_end(n.start(), e);
735      }
736      _ => {}
737    }
738
739    n.test.visit_with(self);
740  }
741
742  fn visit_try_stmt(&mut self, n: &TryStmt) {
743    let old_throw = self.scope.may_throw;
744
745    let prev_end = self.scope.end;
746
747    self.scope.may_throw = false;
748    n.block.visit_with(self);
749
750    let try_block_end = self.scope.end;
751    let try_block_may_throw = self.scope.may_throw;
752
753    if let Some(handler) = &n.handler {
754      if try_block_may_throw {
755        self.scope.end = prev_end;
756      }
757      self.scope.may_throw = false;
758      handler.visit_with(self);
759
760      if try_block_may_throw {
761        match (try_block_end, self.scope.end) {
762          (
763            Some(End::Forced {
764              ret: false,
765              throw: true,
766              infinite_loop: false,
767            }),
768            _,
769          ) => {}
770          (Some(x), Some(y)) if x.is_forced() && y.is_forced() => {
771            // This `unwrap` is safe; `x` and `y` are surely `Some(End::Forced { .. })`
772            self.scope.end = Some(x.merge_forced(y).unwrap());
773          }
774          (_, Some(y)) if y.is_forced() => {
775            self.scope.end = try_block_end;
776          }
777          (None | Some(End::Continue), Some(End::Break)) => {
778            self.scope.end = try_block_end;
779          }
780          _ => {}
781        }
782      } else {
783        self.scope.end = try_block_end;
784      }
785    }
786
787    if let Some(finalizer) = &n.finalizer {
788      let try_catch_end = self.scope.end;
789      self.scope.end = prev_end;
790      self.with_child_scope(BlockKind::Finally, finalizer.start(), |a| {
791        n.finalizer.visit_with(a);
792      });
793      match (try_catch_end, self.scope.end) {
794        (Some(x), Some(End::Break)) if x.is_forced() => {
795          self.scope.end = Some(x);
796        }
797        (Some(x), None | Some(End::Continue)) => {
798          self.scope.end = Some(x);
799        }
800        _ => {}
801      }
802    }
803
804    if let Some(end) = self.scope.end {
805      self.mark_as_end(n.start(), end);
806    }
807    self.scope.may_throw |= old_throw;
808  }
809
810  fn visit_labeled_stmt(&mut self, n: &LabeledStmt) {
811    self.with_child_scope(BlockKind::Label(n.label.to_id()), n.start(), |a| {
812      a.visit_stmt_or_block(&n.body);
813    });
814  }
815}