Repository: sveltejs/svelte (JavaScript)
Date: 2019-01-19 02:42:03+00:00
Task type: Refactoring

Issue:
Track which variables need to be reactive
[REPL](https://v3.svelte.technology/repl?version=3.0.0-alpha16&gist=3a9377863be9b866a1942cdce08a75bf). We're not taking full advantage of the information available to us. This code...

```html
<script>
  let name = 'world';
</script>

<h1>Hello {name}!</h1>
```

...should result in this output...

```diff
function create_fragment($$, ctx) {
  var h1, text0, text1, text2, current;

  return {
    c() {
      h1 = createElement("h1");
      text0 = createText("Hello ");
      text1 = createText(ctx.name);
      text2 = createText("!");
    },

    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, text0);
      append(h1, text1);
      append(h1, text2);
      current = true;
    },

-    p(changed, ctx) {
-      if (changed.name) {
-        setData(text1, ctx.name);
-      }
-    },
+    p: noop,

    i(target, anchor) {
      if (current) return;
      this.m(target, anchor);
    },

    o: run,

    d(detach) {
      if (detach) {
        detachNode(h1);
      }
    }
  };
}
```

...since there's no way `name` could change.

Stretch goal: `let name` should be hoisted out of the instance altogether:

```diff
function create_fragment($$, ctx) {
  var h1, text0, text1, text2, current;

  return {
    c() {
      h1 = createElement("h1");
      text0 = createText("Hello ");
-      text1 = createText(ctx.name);
+      text1 = createText(name);
      text2 = createText("!");
    },

    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, text0);
      append(h1, text1);
      append(h1, text2);
      current = true;
    },

-    p(changed, ctx) {
-      if (changed.name) {
-        setData(text1, ctx.name);
-      }
-    },
+    p: noop,

    i(target, anchor) {
      if (current) return;
      this.m(target, anchor);
    },

    o: run,

    d(detach) {
      if (detach) {
        detachNode(h1);
      }
    }
  };
}

+let name = 'world';
```

This is what happens if it's a `const` instead. In other words, rather than determining a variable's reactivity based on `const` and `let` (which causes other bugs — #1917), it should be based on how the variable is actually used.


Issue discussion:
Playing around, I extended `TemplateScope` to start tracking which names are mutated, which allows code that sets up an update to check its deps for known mutation before doing so. The first pass is quite rough, and there are a fair number of test failures. The thing that jumped out most for me though, were the component prop binding test failures. ~~What happens here with `<Hello bind:name />`?~~
So it turns out I was confusing myself (not hard to do) with a scriptless component. With that cleared up, I think tracking mutation along the template stack will probably work out ok, so I'll keep poking at it to see what shakes out.
> In other words, rather than determining a variable's reactivity based on const and let (which causes other bugs — #1917), it should be based on how the variable is actually used.

+1 as for CoffeeScript users (I'm a proud one) transpiled code only uses `var` instead of `let` or `const`. 

Solution patch:
diff --git a/src/compile/Component.ts b/src/compile/Component.ts
--- a/src/compile/Component.ts
+++ b/src/compile/Component.ts
@@ -750,18 +750,18 @@ export default class Component {
 	}
 
 	hoist_instance_declarations() {
-		// we can safely hoist `const` declarations that are
+		// we can safely hoist variable declarations that are
 		// initialised to literals, and functions that don't
 		// reference instance variables other than other
 		// hoistable functions. TODO others?
 
-		const { hoistable_names, hoistable_nodes, imported_declarations } = this;
+		const { hoistable_names, hoistable_nodes, imported_declarations, instance_scope: scope } = this;
 
 		const top_level_function_declarations = new Map();
 
 		this.instance_script.content.body.forEach(node => {
-			if (node.kind === 'const') { // TODO or let or var, if never reassigned in <script> or template
-				if (node.declarations.every(d => d.init.type === 'Literal')) {
+			if (node.type === 'VariableDeclaration') {
+				if (node.declarations.every(d => d.init && d.init.type === 'Literal' && !this.mutable_props.has(d.id.name))) {
 					node.declarations.forEach(d => {
 						hoistable_names.add(d.id.name);
 					});


Test patch:
diff --git a/test/js/samples/hoisted-let/expected.js b/test/js/samples/hoisted-let/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hoisted-let/expected.js
@@ -0,0 +1,41 @@
+/* generated by Svelte vX.Y.Z */
+import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
+
+function create_fragment($$, ctx) {
+	var b, text_value = get_answer(), text;
+
+	return {
+		c() {
+			b = createElement("b");
+			text = createText(text_value);
+		},
+
+		m(target, anchor) {
+			insert(target, b, anchor);
+			append(b, text);
+		},
+
+		p: noop,
+		i: noop,
+		o: noop,
+
+		d(detach) {
+			if (detach) {
+				detachNode(b);
+			}
+		}
+	};
+}
+
+let ANSWER = 42;
+
+function get_answer() { return ANSWER; }
+
+class SvelteComponent extends SvelteComponent_1 {
+	constructor(options) {
+		super();
+		init(this, options, identity, create_fragment, safe_not_equal);
+	}
+}
+
+export default SvelteComponent;
\ No newline at end of file
diff --git a/test/js/samples/hoisted-let/input.html b/test/js/samples/hoisted-let/input.html
new file mode 100644
--- /dev/null
+++ b/test/js/samples/hoisted-let/input.html
@@ -0,0 +1,6 @@
+<script>
+	let ANSWER = 42;
+	function get_answer() { return ANSWER; }
+</script>
+
+<b>{get_answer()}</b>
\ No newline at end of file
diff --git a/test/js/samples/if-block-no-update/expected.js b/test/js/samples/if-block-no-update/expected.js
--- a/test/js/samples/if-block-no-update/expected.js
+++ b/test/js/samples/if-block-no-update/expected.js
@@ -83,7 +83,7 @@ function create_fragment($$, ctx) {
 
 		d(detach) {
 			if_block.d(detach);
-			
+
 			if (detach) {
 				detachNode(if_block_anchor);
 			}
diff --git a/test/js/samples/if-block-simple/expected.js b/test/js/samples/if-block-simple/expected.js
--- a/test/js/samples/if-block-simple/expected.js
+++ b/test/js/samples/if-block-simple/expected.js
@@ -57,7 +57,7 @@ function create_fragment($$, ctx) {
 
 		d(detach) {
 			if (if_block) if_block.d(detach);
-			
+
 			if (detach) {
 				detachNode(if_block_anchor);
 			}
diff --git a/test/js/samples/instrumentation-template-if-no-block/expected.js b/test/js/samples/instrumentation-template-if-no-block/expected.js
--- a/test/js/samples/instrumentation-template-if-no-block/expected.js
+++ b/test/js/samples/instrumentation-template-if-no-block/expected.js
@@ -11,7 +11,7 @@ function create_fragment($$, ctx) {
 			text1 = createText("\n\n");
 			p = createElement("p");
 			text2 = createText("x: ");
-			text3 = createText(ctx.x);
+			text3 = createText(x);
 			dispose = addListener(button, "click", ctx.click_handler);
 		},
 
@@ -25,7 +25,7 @@ function create_fragment($$, ctx) {
 
 		p(changed, ctx) {
 			if (changed.x) {
-				setData(text3, ctx.x);
+				setData(text3, x);
 			}
 		},
 
@@ -44,14 +44,15 @@ function create_fragment($$, ctx) {
 	};
 }
 
+let x = 0;
+
 function instance($$self, $$props, $$invalidate) {
-	let x = 0;
 
 	function click_handler() {
 		if (true) { x += 1; $$invalidate('x', x); }
 	}
 
-	return { x, click_handler };
+	return { click_handler };
 }
 
 class SvelteComponent extends SvelteComponent_1 {
diff --git a/test/js/samples/non-mutable-reference/expected.js b/test/js/samples/non-mutable-reference/expected.js
--- a/test/js/samples/non-mutable-reference/expected.js
+++ b/test/js/samples/non-mutable-reference/expected.js
@@ -1,5 +1,5 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, init, insert, noop, safe_not_equal } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, append, createElement, createText, detachNode, identity, init, insert, noop, safe_not_equal } from "svelte/internal";
 
 function create_fragment($$, ctx) {
 	var h1, text0, text1, text2;
@@ -8,7 +8,7 @@ function create_fragment($$, ctx) {
 		c() {
 			h1 = createElement("h1");
 			text0 = createText("Hello ");
-			text1 = createText(ctx.name);
+			text1 = createText(name);
 			text2 = createText("!");
 		},
 
@@ -31,16 +31,12 @@ function create_fragment($$, ctx) {
 	};
 }
 
-function instance($$self) {
-	let name = 'world';
-
-	return { name };
-}
+let name = 'world';
 
 class SvelteComponent extends SvelteComponent_1 {
 	constructor(options) {
 		super();
-		init(this, options, instance, create_fragment, safe_not_equal);
+		init(this, options, identity, create_fragment, safe_not_equal);
 	}
 }
 
diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js
--- a/test/js/samples/use-elements-as-anchors/expected.js
+++ b/test/js/samples/use-elements-as-anchors/expected.js
@@ -238,7 +238,7 @@ function create_fragment($$, ctx) {
 			}
 
 			if (if_block4) if_block4.d(detach);
-			
+
 			if (detach) {
 				detachNode(if_block4_anchor);
 			}