From ad66fbdfa2d39e39c2a7d7411e27cb61ada2b648 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 14 Dec 2022 09:56:24 +0100 Subject: [PATCH] Detect infinite loops --- src/model/eval.rs | 37 ++++++++++++++++++++++++++++++++++-- tests/typ/compiler/while.typ | 12 +++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/model/eval.rs b/src/model/eval.rs index 53a393c33..ef9ba0a8c 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -16,7 +16,7 @@ use crate::diag::{ }; use crate::geom::{Abs, Angle, Em, Fr, Ratio}; use crate::syntax::ast::AstNode; -use crate::syntax::{ast, Source, SourceId, Span, Spanned, Unit}; +use crate::syntax::{ast, Source, SourceId, Span, Spanned, SyntaxKind, SyntaxNode, Unit}; use crate::util::{format_eco, EcoString, PathExt}; use crate::World; @@ -994,12 +994,25 @@ impl Eval for ast::WhileLoop { type Output = Value; fn eval(&self, vm: &mut Vm) -> SourceResult { + const MAX_ITERS: usize = 10_000; + let flow = vm.flow.take(); let mut output = Value::None; + let mut i = 0; let condition = self.condition(); + let body = self.body(); + while condition.eval(vm)?.cast::().at(condition.span())? { - let body = self.body(); + if i == 0 + && is_invariant(condition.as_untyped()) + && !can_diverge(body.as_untyped()) + { + bail!(condition.span(), "condition is always true"); + } else if i >= MAX_ITERS { + bail!(self.span(), "loop seems to be infinite"); + } + let value = body.eval(vm)?; output = ops::join(output, value).at(body.span())?; @@ -1012,6 +1025,8 @@ impl Eval for ast::WhileLoop { Some(Flow::Return(..)) => break, None => {} } + + i += 1; } if flow.is_some() { @@ -1022,6 +1037,24 @@ impl Eval for ast::WhileLoop { } } +/// Whether the expression always evaluates to the same value. +fn is_invariant(expr: &SyntaxNode) -> bool { + match expr.cast() { + Some(ast::Expr::Ident(_)) => false, + Some(ast::Expr::MethodCall(call)) => { + is_invariant(call.target().as_untyped()) + && is_invariant(call.args().as_untyped()) + } + _ => expr.children().all(is_invariant), + } +} + +/// Whether the expression contains a break or return. +fn can_diverge(expr: &SyntaxNode) -> bool { + matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return) + || expr.children().any(can_diverge) +} + impl Eval for ast::ForLoop { type Output = Value; diff --git a/tests/typ/compiler/while.typ b/tests/typ/compiler/while.typ index 5dc5ae417..3c28a32a1 100644 --- a/tests/typ/compiler/while.typ +++ b/tests/typ/compiler/while.typ @@ -34,11 +34,13 @@ #while [nope] [nope] --- -// Make sure that we terminate and don't complain multiple times. -#while true { - // Error: 3-7 unknown variable - nope -} +// Error: 8-25 condition is always true +#while 2 < "hello".len() {} + +--- +// Error: 2:1-2:24 loop seems to be infinite +#let i = 1 +#while i > 0 { i += 1 } --- // Error: 7 expected expression