Repository: sveltejs/svelte (JavaScript)
Date: 2021-11-27 15:31:36+00:00
Task type: Bug Fix

Issue:
`Infinite loop detected` triggered in async loops
[REPL example](https://svelte.dev/repl/cd92d7b053f04ce981ad9a3871cdc5cd?version=3.44.2)

The code is roughly as follows:

```svelte
<script>
  let pages = []
  $: if (search.length > 0) {
    while (! allPagesLoaded) {
      pages = [...pages, await loadPage(previousPage)]
    }
  }	
</script>
```

[Exact code](https://github.com/GauBen/svelte-tenor/blob/main/src/lib/Search.svelte#L56)

Since the `loadPage` function is slow to complete, the loop guard throws an exception, despite the loop not being infinite.



Issue discussion:
Moving this to the `svelte` repo as a feature request to update the loop guard feature ... somehow. Maybe disable it for loops inside async functions?

Solution patch:
diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts
--- a/src/compiler/compile/Component.ts
+++ b/src/compiler/compile/Component.ts
@@ -24,7 +24,7 @@ import TemplateScope from './nodes/shared/TemplateScope';
 import fuzzymatch from '../utils/fuzzymatch';
 import get_object from './utils/get_object';
 import Slot from './nodes/Slot';
-import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration } from 'estree';
+import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration, FunctionDeclaration, FunctionExpression } from 'estree';
 import add_to_set from './utils/add_to_set';
 import check_graph_for_cycles from './utils/check_graph_for_cycles';
 import { print, b } from 'code-red';
@@ -758,12 +758,13 @@ export default class Component {
 		};
 		let scope_updated = false;
 
-		let generator_count = 0;
+		const current_function_stack = [];
+		let current_function: FunctionDeclaration | FunctionExpression = null;
 
 		walk(content, {
 			enter(node: Node, parent: Node, prop, index) {
-				if ((node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') && node.generator === true) {
-					generator_count++;
+				if ((node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')) {
+					current_function_stack.push(current_function = node);
 				}
 
 				if (map.has(node)) {
@@ -792,12 +793,13 @@ export default class Component {
 			},
 
 			leave(node: Node) {
-				if ((node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') && node.generator === true) {
-					generator_count--;
+				if ((node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')) {
+					current_function_stack.pop();
+					current_function = current_function_stack[current_function_stack.length - 1];
 				}
 
 				// do it on leave, to prevent infinite loop
-				if (component.compile_options.dev && component.compile_options.loopGuardTimeout > 0 && generator_count <= 0) {
+				if (component.compile_options.dev && component.compile_options.loopGuardTimeout > 0 && (!current_function || (!current_function.generator && !current_function.async))) {
 					const to_replace_for_loop_protect = component.loop_protect(node, scope, component.compile_options.loopGuardTimeout);
 					if (to_replace_for_loop_protect) {
 						this.replace(to_replace_for_loop_protect);


Test patch:
diff --git a/test/runtime/samples/loop-protect-async-opt-out/_config.js b/test/runtime/samples/loop-protect-async-opt-out/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/loop-protect-async-opt-out/_config.js
@@ -0,0 +1,9 @@
+export default {
+	compileOptions: {
+		dev: true,
+		loopGuardTimeout: 1
+	},
+	async test({ component }) {
+		await component.run_async_function();
+	}
+};
diff --git a/test/runtime/samples/loop-protect-async-opt-out/main.svelte b/test/runtime/samples/loop-protect-async-opt-out/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/loop-protect-async-opt-out/main.svelte
@@ -0,0 +1,12 @@
+<script>
+	export async function run_async_function() {
+		await loop();
+	}
+
+	async function loop() {
+		let i = 0;
+		while (i ++ < 5) {
+			await new Promise(resolve => setTimeout(resolve, 20));
+		}
+	}
+</script>
diff --git a/test/runtime/samples/loop-protect-generator-opt-out/_config.js b/test/runtime/samples/loop-protect-generator-opt-out/_config.js
--- a/test/runtime/samples/loop-protect-generator-opt-out/_config.js
+++ b/test/runtime/samples/loop-protect-generator-opt-out/_config.js
@@ -2,5 +2,8 @@ export default {
 	compileOptions: {
 		dev: true,
 		loopGuardTimeout: 1
+	},
+	async test({ component }) {
+		await component.run_generator();
 	}
 };
diff --git a/test/runtime/samples/loop-protect-generator-opt-out/main.svelte b/test/runtime/samples/loop-protect-generator-opt-out/main.svelte
--- a/test/runtime/samples/loop-protect-generator-opt-out/main.svelte
+++ b/test/runtime/samples/loop-protect-generator-opt-out/main.svelte
@@ -1,10 +1,12 @@
 <script>
-	const it = gen();
-	it.next();
+	export async function run_generator() {
+		const it = gen();
+		it.next();
+
+		await new Promise(resolve => setTimeout(resolve, 20));
 
-	setTimeout(() => {
 		it.next();
-	}, 20)
+	}
 
 	function* gen() {
 		while (true) {