diff --git a/src/Stats.ts b/src/Stats.ts
index 33f651b2fa..5236e6a85c 100644
--- a/src/Stats.ts
+++ b/src/Stats.ts
@@ -97,11 +97,18 @@ export default class Stats {
 		});
 
 		return {
-			props: component.props.map(prop => prop.as),
 			timings,
 			warnings: this.warnings,
-			imports,
-			templateReferences: component && component.template_references
+			vars: component.vars.filter(variable => !variable.global && !variable.implicit && !variable.internal).map(variable => ({
+				name: variable.name,
+				export_name: variable.export_name || null,
+				injected: variable.injected || false,
+				module: variable.module || false,
+				mutated: variable.mutated || false,
+				reassigned: variable.reassigned || false,
+				referenced: variable.referenced || false,
+				writable: variable.writable || false
+			}))
 		};
 	}
 
diff --git a/src/compile/Component.ts b/src/compile/Component.ts
index c7ca6b7feb..42d3f7a449 100644
--- a/src/compile/Component.ts
+++ b/src/compile/Component.ts
@@ -11,11 +11,10 @@ import Stylesheet from './css/Stylesheet';
 import { test } from '../config';
 import Fragment from './nodes/Fragment';
 import internal_exports from './internal-exports';
-import { Node, Ast, CompileOptions } from '../interfaces';
+import { Node, Ast, CompileOptions, Var } from '../interfaces';
 import error from '../utils/error';
 import getCodeFrame from '../utils/getCodeFrame';
 import flattenReference from '../utils/flattenReference';
-import addToSet from '../utils/addToSet';
 import isReference from 'is-reference';
 import TemplateScope from './nodes/shared/TemplateScope';
 import fuzzymatch from '../utils/fuzzymatch';
@@ -56,31 +55,22 @@ export default class Component {
 	namespace: string;
 	tag: string;
 
-	instance_script: Node;
-	module_script: Node;
+	vars: Var[] = [];
+	var_lookup: Map<string, Var> = new Map();
 
 	imports: Node[] = [];
 	module_javascript: string;
 	javascript: string;
 
-	declarations: string[] = [];
-	props: Array<{ name: string, as: string }> = [];
-	writable_declarations: Set<string> = new Set();
-	initialised_declarations: Set<string> = new Set();
-	imported_declarations: Set<string> = new Set();
-	hoistable_names: Set<string> = new Set();
 	hoistable_nodes: Set<Node> = new Set();
 	node_for_declaration: Map<string, Node> = new Map();
-	module_exports: Array<{ name: string, as: string }> = [];
 	partly_hoisted: string[] = [];
 	fully_hoisted: string[] = [];
 	reactive_declarations: Array<{ assignees: Set<string>, dependencies: Set<string>, snippet: string }> = [];
 	reactive_declaration_nodes: Set<Node> = new Set();
 	has_reactive_assignments = false;
-	mutable_props: Set<string> = new Set();
 
 	indirectDependencies: Map<string, Set<string>> = new Map();
-	template_references: Set<string> = new Set();
 
 	file: string;
 	locate: (c: number) => { line: number, column: number };
@@ -93,7 +83,6 @@ export default class Component {
 
 	stylesheet: Stylesheet;
 
-	userVars: Set<string> = new Set();
 	aliases: Map<string, string> = new Map();
 	usedNames: Set<string> = new Set();
 
@@ -122,26 +111,6 @@ export default class Component {
 		this.stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
 		this.stylesheet.validate(this);
 
-		const module_scripts = ast.js.filter(script => this.get_context(script) === 'module');
-		const instance_scripts = ast.js.filter(script => this.get_context(script) === 'default');
-
-		if (module_scripts.length > 1) {
-			this.error(module_scripts[1], {
-				code: `invalid-script`,
-				message: `A component can only have one <script context="module"> element`
-			});
-		}
-
-		if (instance_scripts.length > 1) {
-			this.error(instance_scripts[1], {
-				code: `invalid-script`,
-				message: `A component can only have one instance-level <script> element`
-			});
-		}
-
-		this.module_script = module_scripts[0];
-		this.instance_script = instance_scripts[0];
-
 		this.meta = process_meta(this, this.ast.html.children);
 		this.namespace = namespaces[this.meta.namespace] || this.meta.namespace;
 
@@ -169,22 +138,43 @@ export default class Component {
 		if (!options.customElement) this.stylesheet.reify();
 
 		this.stylesheet.warnOnUnusedSelectors(stats);
+	}
+
+	add_var(variable: Var) {
+		// TODO remove this
+		if (this.var_lookup.has(variable.name)) {
+			throw new Error(`dupe: ${variable.name}`);
+		}
+
+		this.vars.push(variable);
+		this.var_lookup.set(variable.name, variable);
+	}
 
-		if (!this.instance_script) {
-			const props = [...this.template_references];
-			this.declarations.push(...props);
-			addToSet(this.mutable_props, this.template_references);
-			addToSet(this.writable_declarations, this.template_references);
-			addToSet(this.userVars, this.template_references);
+	add_reference(name: string) {
+		const variable = this.var_lookup.get(name);
 
-			this.props = props.map(name => ({
+		if (variable) {
+			variable.referenced = true;
+		} else if (name[0] === '$') {
+			this.add_var({
 				name,
-				as: name
-			}));
-		}
+				injected: true,
+				referenced: true,
+				mutated: true,
+				writable: true
+			});
 
-		// tell the root fragment scope about all of the mutable names we know from the script
-		this.mutable_props.forEach(name => this.fragment.scope.mutables.add(name));
+			this.add_reference(name.slice(1));
+		} else if (!this.ast.instance) {
+			this.add_var({
+				name,
+				export_name: name,
+				implicit: true,
+				mutated: false,
+				referenced: true,
+				writable: true
+			});
+		}
 	}
 
 	addSourcemapLocations(node: Node) {
@@ -243,7 +233,10 @@ export default class Component {
 			options.sveltePath,
 			importedHelpers,
 			this.imports,
-			this.module_exports,
+			this.vars.filter(variable => variable.module && variable.export_name).map(variable => ({
+				name: variable.name,
+				as: variable.export_name
+			})),
 			this.source
 		);
 
@@ -313,7 +306,7 @@ export default class Component {
 		for (
 			let i = 1;
 			reservedNames.has(alias) ||
-			this.userVars.has(alias) ||
+			this.var_lookup.has(alias) ||
 			this.usedNames.has(alias);
 			alias = `${name}_${i++}`
 		);
@@ -329,7 +322,7 @@ export default class Component {
 		}
 
 		reservedNames.forEach(add);
-		this.userVars.forEach(add);
+		this.var_lookup.forEach((value, key) => add(key));
 
 		return (name: string) => {
 			if (test) name = `${name}$`;
@@ -396,7 +389,34 @@ export default class Component {
 		});
 	}
 
-	extract_imports_and_exports(content, imports, exports) {
+	extract_imports(content, is_module: boolean) {
+		const { code } = this;
+
+		content.body.forEach(node => {
+			if (node.type === 'ImportDeclaration') {
+				// imports need to be hoisted out of the IIFE
+				removeNode(code, content.start, content.end, content.body, node);
+				this.imports.push(node);
+
+				node.specifiers.forEach((specifier: Node) => {
+					if (specifier.local.name[0] === '$') {
+						this.error(specifier.local, {
+							code: 'illegal-declaration',
+							message: `The $ prefix is reserved, and cannot be used for variable and import names`
+						});
+					}
+
+					this.add_var({
+						name: specifier.local.name,
+						module: is_module,
+						hoistable: true
+					});
+				});
+			}
+		});
+	}
+
+	extract_exports(content, is_module: boolean) {
 		const { code } = this;
 
 		content.body.forEach(node => {
@@ -412,44 +432,31 @@ export default class Component {
 					if (node.declaration.type === 'VariableDeclaration') {
 						node.declaration.declarations.forEach(declarator => {
 							extractNames(declarator.id).forEach(name => {
-								exports.push({ name, as: name });
-								this.mutable_props.add(name);
+								const variable = this.var_lookup.get(name);
+								variable.export_name = name;
 							});
 						});
 					} else {
 						const { name } = node.declaration.id;
-						exports.push({ name, as: name });
+
+						const variable = this.var_lookup.get(name);
+						variable.export_name = name;
 					}
 
 					code.remove(node.start, node.declaration.start);
 				} else {
 					removeNode(code, content.start, content.end, content.body, node);
 					node.specifiers.forEach(specifier => {
-						exports.push({
-							name: specifier.local.name,
-							as: specifier.exported.name
-						});
+						const variable = this.var_lookup.get(specifier.local.name);
+
+						if (variable) {
+							variable.export_name = specifier.exported.name;
+						} else {
+							// TODO what happens with `export { Math }` or some other global?
+						}
 					});
 				}
 			}
-
-			// imports need to be hoisted out of the IIFE
-			else if (node.type === 'ImportDeclaration') {
-				removeNode(code, content.start, content.end, content.body, node);
-				imports.push(node);
-
-				node.specifiers.forEach((specifier: Node) => {
-					if (specifier.local.name[0] === '$') {
-						this.error(specifier.local, {
-							code: 'illegal-declaration',
-							message: `The $ prefix is reserved, and cannot be used for variable and import names`
-						});
-					}
-
-					this.userVars.add(specifier.local.name);
-					this.imported_declarations.add(specifier.local.name);
-				});
-			}
 		});
 	}
 
@@ -491,7 +498,7 @@ export default class Component {
 	}
 
 	walk_module_js() {
-		const script = this.module_script;
+		const script = this.ast.module;
 		if (!script) return;
 
 		this.addSourcemapLocations(script.content);
@@ -506,15 +513,35 @@ export default class Component {
 					message: `The $ prefix is reserved, and cannot be used for variable and import names`
 				});
 			}
+
+			if (!/Import/.test(node.type)) {
+				const kind = node.type === 'VariableDeclaration'
+					? node.kind
+					: node.type === 'ClassDeclaration'
+						? 'class'
+						: node.type === 'FunctionDeclaration'
+							? 'function'
+							: null;
+
+				// sanity check
+				if (!kind) throw new Error(`Unknown declaration type ${node.type}`);
+
+				this.add_var({
+					name,
+					module: true,
+					hoistable: true
+				});
+			}
 		});
 
-		this.extract_imports_and_exports(script.content, this.imports, this.module_exports);
+		this.extract_imports(script.content, true);
+		this.extract_exports(script.content, true);
 		remove_indentation(this.code, script.content);
 		this.module_javascript = this.extract_javascript(script);
 	}
 
 	walk_instance_js_pre_template() {
-		const script = this.instance_script;
+		const script = this.ast.instance;
 		if (!script) return;
 
 		this.addSourcemapLocations(script.content);
@@ -530,28 +557,56 @@ export default class Component {
 					message: `The $ prefix is reserved, and cannot be used for variable and import names`
 				});
 			}
-		});
 
-		instance_scope.declarations.forEach((node, name) => {
-			this.userVars.add(name);
-			this.declarations.push(name);
+			if (!/Import/.test(node.type)) {
+				const kind = node.type === 'VariableDeclaration'
+					? node.kind
+					: node.type === 'ClassDeclaration'
+						? 'class'
+						: node.type === 'FunctionDeclaration'
+							? 'function'
+							: null;
+
+				// sanity check
+				if (!kind) throw new Error(`Unknown declaration type ${node.type}`);
+
+				this.add_var({
+					name,
+					initialised: instance_scope.initialised_declarations.has(name),
+					writable: kind === 'var' || kind === 'let'
+				});
+			}
 
 			this.node_for_declaration.set(name, node);
 		});
 
-		this.writable_declarations = instance_scope.writable_declarations;
-		this.initialised_declarations = instance_scope.initialised_declarations;
-
 		globals.forEach(name => {
-			this.userVars.add(name);
+			if (this.module_scope && this.module_scope.declarations.has(name)) return;
+
+			if (name[0] === '$') {
+				this.add_var({
+					name,
+					injected: true,
+					mutated: true,
+					writable: true
+				});
+
+				this.add_reference(name.slice(1));
+			} else {
+				this.add_var({
+					name,
+					global: true
+				});
+			}
 		});
 
+		this.extract_imports(script.content, false);
+		this.extract_exports(script.content, false);
 		this.track_mutations();
-		this.extract_imports_and_exports(script.content, this.imports, this.props);
 	}
 
 	walk_instance_js_post_template() {
-		const script = this.instance_script;
+		const script = this.ast.instance;
 		if (!script) return;
 
 		this.hoist_instance_declarations();
@@ -563,28 +618,43 @@ export default class Component {
 	// TODO merge this with other walks that are independent
 	track_mutations() {
 		const component = this;
-		let { instance_scope: scope, instance_scope_map: map } = this;
+		const { instance_scope, instance_scope_map: map } = this;
+
+		let scope = instance_scope;
 
-		walk(this.instance_script.content, {
+		walk(this.ast.instance.content, {
 			enter(node, parent) {
-				let names;
 				if (map.has(node)) {
 					scope = map.get(node);
 				}
 
+				let names;
+				let deep = false;
+
 				if (node.type === 'AssignmentExpression') {
-					names = node.left.type === 'MemberExpression'
-							? [getObject(node.left).name]
-							: extractNames(node.left);
+					deep = node.left.type === 'MemberExpression';
+
+					names = deep
+						? [getObject(node.left).name]
+						: extractNames(node.left);
 				} else if (node.type === 'UpdateExpression') {
 					names = [getObject(node.argument).name];
 				}
 
 				if (names) {
 					names.forEach(name => {
-						if (scope.has(name)) component.mutable_props.add(name);
+						if (scope.findOwner(name) === instance_scope) {
+							const variable = component.var_lookup.get(name);
+							variable[deep ? 'mutated' : 'reassigned'] = true;
+						}
 					});
 				}
+			},
+
+			leave(node) {
+				if (map.has(node)) {
+					scope = scope.parent;
+				}
 			}
 		})
 	}
@@ -595,7 +665,7 @@ export default class Component {
 		const component = this;
 		let { instance_scope: scope, instance_scope_map: map } = this;
 
-		walk(this.instance_script.content, {
+		walk(this.ast.instance.content, {
 			enter(node, parent) {
 				if (map.has(node)) {
 					scope = map.get(node);
@@ -607,9 +677,6 @@ export default class Component {
 
 					if (name[0] === '$' && !scope.has(name)) {
 						component.warn_if_undefined(object, null);
-
-						// cheeky hack
-						component.template_references.add(name);
 					}
 				}
 			},
@@ -627,17 +694,10 @@ export default class Component {
 		const { code, instance_scope, instance_scope_map: map, meta } = this;
 		let scope = instance_scope;
 
-		// TODO we will probably end up wanting to use this elsewhere
-		const exported = new Set();
-		this.props.forEach(prop => {
-			exported.add(prop.name);
-			this.mutable_props.add(prop.name);
-		});
-
 		const coalesced_declarations = [];
 		let current_group;
 
-		walk(this.instance_script.content, {
+		walk(this.ast.instance.content, {
 			enter(node, parent) {
 				if (/Function/.test(node.type)) {
 					current_group = null;
@@ -650,14 +710,15 @@ export default class Component {
 
 				if (node.type === 'VariableDeclaration') {
 					if (node.kind === 'var' || scope === instance_scope) {
-						let has_meta_props = false;
 						let has_exports = false;
 						let has_only_exports = true;
 
 						node.declarations.forEach(declarator => {
 							extractNames(declarator.id).forEach(name => {
+								const variable = component.var_lookup.get(name);
+
 								if (name === meta.props_object) {
-									if (exported.has(name)) {
+									if (variable.export_name) {
 										component.error(declarator, {
 											code: 'exported-meta-props',
 											message: `Cannot export props binding`
@@ -676,11 +737,9 @@ export default class Component {
 									} else {
 										code.overwrite(declarator.id.end, declarator.end, ' = $$props');
 									}
-
-									has_meta_props = true;
 								}
 
-								if (exported.has(name)) {
+								if (variable.export_name) {
 									has_exports = true;
 								} else {
 									has_only_exports = false;
@@ -724,7 +783,6 @@ export default class Component {
 		});
 
 		coalesced_declarations.forEach(group => {
-			const kind = group[0].kind;
 			let c = 0;
 
 			let combining = false;
@@ -764,16 +822,16 @@ export default class Component {
 		// reference instance variables other than other
 		// hoistable functions. TODO others?
 
-		const { hoistable_names, hoistable_nodes, imported_declarations, instance_scope: scope } = this;
-		const template_scope = this.fragment.scope;
+		const { hoistable_nodes, var_lookup } = this;
 
 		const top_level_function_declarations = new Map();
 
-		this.instance_script.content.body.forEach(node => {
+		this.ast.instance.content.body.forEach(node => {
 			if (node.type === 'VariableDeclaration') {
-				if (node.declarations.every(d => d.init && d.init.type === 'Literal' && !this.mutable_props.has(d.id.name) && !template_scope.containsMutable([d.id.name]))) {
+				if (node.declarations.every(d => d.init && d.init.type === 'Literal' && !this.var_lookup.get(d.id.name).reassigned)) {
 					node.declarations.forEach(d => {
-						hoistable_names.add(d.id.name);
+						const variable = this.var_lookup.get(d.id.name);
+						variable.hoistable = true;
 					});
 
 					hoistable_nodes.add(node);
@@ -823,8 +881,9 @@ export default class Component {
 
 						else if (owner === instance_scope) {
 							if (name === fn_declaration.id.name) return;
-							if (hoistable_names.has(name)) return;
-							if (imported_declarations.has(name)) return;
+
+							const variable = var_lookup.get(name);
+							if (variable.hoistable) return;
 
 							if (top_level_function_declarations.has(name)) {
 								const other_declaration = top_level_function_declarations.get(name);
@@ -860,7 +919,8 @@ export default class Component {
 
 		for (const [name, node] of top_level_function_declarations) {
 			if (!checked.has(node) && is_hoistable(node)) {
-				hoistable_names.add(name);
+				const variable = this.var_lookup.get(name);
+				variable.hoistable = true;
 				hoistable_nodes.add(node);
 
 				remove_indentation(this.code, node);
@@ -875,7 +935,7 @@ export default class Component {
 
 		const unsorted_reactive_declarations = [];
 
-		this.instance_script.content.body.forEach(node => {
+		this.ast.instance.content.body.forEach(node => {
 			if (node.type === 'LabeledStatement' && node.label.name === '$') {
 				this.reactive_declaration_nodes.add(node);
 
@@ -899,7 +959,7 @@ export default class Component {
 							const object = getObject(node);
 							const { name } = object;
 
-							if (name[0] === '$' || component.declarations.indexOf(name) !== -1) {
+							if (name[0] === '$' || component.var_lookup.has(name)) {
 								dependencies.add(name);
 							}
 
@@ -982,11 +1042,12 @@ export default class Component {
 	}
 
 	qualify(name) {
-		if (this.hoistable_names.has(name)) return name;
-		if (this.imported_declarations.has(name)) return name;
-		if (this.declarations.indexOf(name) === -1) return name;
+		const variable = this.var_lookup.get(name);
+
+		if (!variable) return name;
+		if (variable && variable.hoistable) return name;
 
-		this.template_references.add(name); // TODO we can probably remove most other occurrences of this
+		this.add_reference(name); // TODO we can probably remove most other occurrences of this
 		return `ctx.${name}`;
 	}
 
@@ -998,7 +1059,7 @@ export default class Component {
 			this.has_reactive_assignments = true;
 		}
 
-		if (allow_implicit && !this.instance_script) return;
+		if (allow_implicit && !this.ast.instance && !this.ast.module) return;
 		if (this.instance_scope && this.instance_scope.declarations.has(name)) return;
 		if (this.module_scope && this.module_scope.declarations.has(name)) return;
 		if (template_scope && template_scope.names.has(name)) return;
@@ -1009,29 +1070,6 @@ export default class Component {
 			message: `'${name}' is not defined`
 		});
 	}
-
-	get_context(script) {
-		const context = script.attributes.find(attribute => attribute.name === 'context');
-		if (!context) return 'default';
-
-		if (context.value.length !== 1 || context.value[0].type !== 'Text') {
-			this.error(script, {
-				code: 'invalid-script',
-				message: `context attribute must be static`
-			});
-		}
-
-		const value = context.value[0].data;
-
-		if (value !== 'module') {
-			this.error(context, {
-				code: `invalid-script`,
-				message: `If the context attribute is supplied, its value must be "module"`
-			});
-		}
-
-		return value;
-	}
 }
 
 function process_meta(component, nodes) {
diff --git a/src/compile/css/Stylesheet.ts b/src/compile/css/Stylesheet.ts
index 2cfb960862..0a36086765 100644
--- a/src/compile/css/Stylesheet.ts
+++ b/src/compile/css/Stylesheet.ts
@@ -265,15 +265,15 @@ export default class Stylesheet {
 		this.nodesWithCssClass = new Set();
 		this.nodesWithRefCssClass = new Map();
 
-		if (ast.css[0] && ast.css[0].children.length) {
-			this.id = `svelte-${hash(ast.css[0].content.styles)}`;
+		if (ast.css && ast.css.children.length) {
+			this.id = `svelte-${hash(ast.css.content.styles)}`;
 
 			this.hasStyles = true;
 
 			const stack: (Rule | Atrule)[] = [];
 			let currentAtrule: Atrule = null;
 
-			walk(ast.css[0], {
+			walk(ast.css, {
 				enter: (node: Node) => {
 					if (node.type === 'Atrule') {
 						const last = stack[stack.length - 1];
diff --git a/src/compile/nodes/Attribute.ts b/src/compile/nodes/Attribute.ts
index 88aaadff26..3e1a08855c 100644
--- a/src/compile/nodes/Attribute.ts
+++ b/src/compile/nodes/Attribute.ts
@@ -34,7 +34,7 @@ export default class Attribute extends Node {
 			this.isSynthetic = false;
 
 			this.expression = new Expression(component, this, scope, info.expression);
-			this.dependencies = this.expression.dynamic_dependencies;
+			this.dependencies = this.expression.dependencies;
 			this.chunks = null;
 
 			this.isDynamic = true; // TODO not necessarily
@@ -59,7 +59,7 @@ export default class Attribute extends Node {
 
 					const expression = new Expression(component, this, scope, node.expression);
 
-					addToSet(this.dependencies, expression.dynamic_dependencies);
+					addToSet(this.dependencies, expression.dependencies);
 					return expression;
 				});
 
@@ -73,6 +73,19 @@ export default class Attribute extends Node {
 		}
 	}
 
+	get_dependencies() {
+		if (this.isSpread) return this.expression.dynamic_dependencies();
+
+		const dependencies = new Set();
+		this.chunks.forEach(chunk => {
+			if (chunk.type === 'Expression') {
+				addToSet(dependencies, chunk.dynamic_dependencies());
+			}
+		});
+
+		return [...dependencies];
+	}
+
 	getValue() {
 		if (this.isTrue) return true;
 		if (this.chunks.length === 0) return `""`;
diff --git a/src/compile/nodes/Binding.ts b/src/compile/nodes/Binding.ts
index 2432478750..050b7f131c 100644
--- a/src/compile/nodes/Binding.ts
+++ b/src/compile/nodes/Binding.ts
@@ -2,6 +2,7 @@ import Node from './shared/Node';
 import getObject from '../../utils/getObject';
 import Expression from './shared/Expression';
 import Component from '../Component';
+import TemplateScope from './shared/TemplateScope';
 
 export default class Binding extends Node {
 	name: string;
@@ -10,7 +11,7 @@ export default class Binding extends Node {
 	obj: string;
 	prop: string;
 
-	constructor(component: Component, parent, scope, info) {
+	constructor(component: Component, parent, scope: TemplateScope, info) {
 		super(component, parent, scope, info);
 
 		if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {
@@ -30,7 +31,21 @@ export default class Binding extends Node {
 		this.isContextual = scope.names.has(name);
 
 		// make sure we track this as a mutable ref
-		scope.setMutable(name);
+		if (this.isContextual) {
+			scope.dependenciesForName.get(name).forEach(name => {
+				const variable = component.var_lookup.get(name);
+				variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
+			});
+		} else {
+			const variable = component.var_lookup.get(name);
+
+			if (!variable) component.error(this.expression.node, {
+				code: 'binding-undeclared',
+				message: `${name} is not declared`
+			});
+
+			variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
+		}
 
 		if (this.expression.node.type === 'MemberExpression') {
 			prop = `[✂${this.expression.node.property.start}-${this.expression.node.property.end}✂]`;
diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts
index 362dbb784f..44e91b6408 100644
--- a/src/compile/nodes/Element.ts
+++ b/src/compile/nodes/Element.ts
@@ -92,7 +92,6 @@ export default class Element extends Node {
 	constructor(component, parent, scope, info: any) {
 		super(component, parent, scope, info);
 		this.name = info.name;
-		this.scope = scope;
 
 		const parentElement = parent.findNearest(/^Element/);
 		this.namespace = this.name === 'svg' ?
@@ -196,7 +195,7 @@ export default class Element extends Node {
 				const dependencies = new Set([l.name]);
 
 				l.names.forEach(name => {
-					this.scope.add(name, dependencies);
+					this.scope.add(name, dependencies, this);
 				});
 			});
 		} else {
diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts
index fe29c6d1fd..e426efa79c 100644
--- a/src/compile/nodes/EventHandler.ts
+++ b/src/compile/nodes/EventHandler.ts
@@ -41,7 +41,12 @@ export default class EventHandler extends Node {
 			}
 		} else {
 			const name = component.getUniqueName(`${this.name}_handler`);
-			component.declarations.push(name);
+
+			component.add_var({
+				name,
+				internal: true,
+				referenced: true
+			});
 
 			component.partly_hoisted.push(deindent`
 				function ${name}(event) {
@@ -57,7 +62,7 @@ export default class EventHandler extends Node {
 	render(block: Block) {
 		if (this.expression) return this.expression.render(block);
 
-		this.component.template_references.add(this.handler_name);
+		// this.component.add_reference(this.handler_name);
 		return `ctx.${this.handler_name}`;
 	}
 }
\ No newline at end of file
diff --git a/src/compile/nodes/InlineComponent.ts b/src/compile/nodes/InlineComponent.ts
index 4d3b7332a6..28c7b2594e 100644
--- a/src/compile/nodes/InlineComponent.ts
+++ b/src/compile/nodes/InlineComponent.ts
@@ -24,7 +24,7 @@ export default class InlineComponent extends Node {
 
 		if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
 			component.warn_if_undefined(info, scope);
-			component.template_references.add(info.name);
+			component.add_reference(info.name);
 		}
 
 		this.name = info.name;
diff --git a/src/compile/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts
index 9ec0c92e84..ea2237543a 100644
--- a/src/compile/nodes/shared/Expression.ts
+++ b/src/compile/nodes/shared/Expression.ts
@@ -62,6 +62,7 @@ const precedence: Record<string, (node?: Node) => number> = {
 };
 
 export default class Expression {
+	type = 'Expression';
 	component: Component;
 	owner: Wrapper;
 	node: any;
@@ -69,7 +70,6 @@ export default class Expression {
 	references: Set<string>;
 	dependencies: Set<string> = new Set();
 	contextual_dependencies: Set<string> = new Set();
-	dynamic_dependencies: Set<string> = new Set();
 
 	template_scope: TemplateScope;
 	scope: Scope;
@@ -95,7 +95,7 @@ export default class Expression {
 		this.owner = owner;
 		this.is_synthetic = owner.isSynthetic;
 
-		const { dependencies, contextual_dependencies, dynamic_dependencies } = this;
+		const { dependencies, contextual_dependencies } = this;
 
 		let { map, scope } = createScopes(info);
 		this.scope = scope;
@@ -104,27 +104,6 @@ export default class Expression {
 		const expression = this;
 		let function_expression;
 
-		function add_dependency(name, deep = false) {
-			dependencies.add(name);
-
-			if (!function_expression) {
-				// dynamic_dependencies is used to create `if (changed.foo || ...)`
-				// conditions — it doesn't apply if the dependency is inside a
-				// function, and it only applies if the dependency is writable
-				// or a sub-path of a non-writable
-				if (component.instance_script) {
-					const owner = template_scope.getOwner(name);
-					const is_let = owner && (owner.type === 'InlineComponent' || owner.type === 'Element');
-
-					if (is_let || component.writable_declarations.has(name) || name[0] === '$' || (component.userVars.has(name) && deep)) {
-						dynamic_dependencies.add(name);
-					}
-				} else {
-					dynamic_dependencies.add(name);
-				}
-			}
-		}
-
 		// discover dependencies, but don't change the code yet
 		walk(info, {
 			enter(node: any, parent: any, key: string) {
@@ -143,18 +122,26 @@ export default class Expression {
 					const { name, nodes } = flattenReference(node);
 
 					if (scope.has(name)) return;
-					if (globalWhitelist.has(name) && component.declarations.indexOf(name) === -1) return;
+					if (globalWhitelist.has(name) && !component.var_lookup.has(name)) return;
 
-					if (template_scope.names.has(name)) {
+					if (template_scope.is_let(name)) {
+						if (!function_expression) {
+							dependencies.add(name);
+						}
+					} else if (template_scope.names.has(name)) {
 						expression.usesContext = true;
 
 						contextual_dependencies.add(name);
 
-						template_scope.dependenciesForName.get(name).forEach(name => add_dependency(name, true));
+						if (!function_expression) {
+							template_scope.dependenciesForName.get(name).forEach(name => dependencies.add(name));
+						}
 					} else {
-						add_dependency(name, nodes.length > 1);
-						component.template_references.add(name);
+						if (!function_expression) {
+							dependencies.add(name);
+						}
 
+						component.add_reference(name);
 						component.warn_if_undefined(nodes[0], template_scope, true);
 					}
 
@@ -162,17 +149,36 @@ export default class Expression {
 				}
 
 				// track any assignments from template expressions as mutable
+				let names;
+				let deep = false;
+
 				if (function_expression) {
 					if (node.type === 'AssignmentExpression') {
-						const names = node.left.type === 'MemberExpression'
+						deep = node.left.type === 'MemberExpression';
+						names = deep
 							? [getObject(node.left).name]
 							: extractNames(node.left);
-						names.forEach(name => template_scope.setMutable(name));
 					} else if (node.type === 'UpdateExpression') {
 						const { name } = getObject(node.argument);
-						template_scope.setMutable(name);
+						names = [name];
 					}
 				}
+
+				if (names) {
+					names.forEach(name => {
+						if (template_scope.names.has(name)) {
+							template_scope.dependenciesForName.get(name).forEach(name => {
+								const variable = component.var_lookup.get(name);
+								if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
+							});
+						} else {
+							component.add_reference(name);
+
+							const variable = component.var_lookup.get(name);
+							if (variable) variable[deep ? 'mutated' : 'reassigned'] = true;
+						}
+					});
+				}
 			},
 
 			leave(node) {
@@ -187,6 +193,18 @@ export default class Expression {
 		});
 	}
 
+	dynamic_dependencies() {
+		return Array.from(this.dependencies).filter(name => {
+			if (this.template_scope.is_let(name)) return true;
+
+			const variable = this.component.var_lookup.get(name);
+			if (!variable) return false;
+
+			if (variable.mutated || variable.reassigned) return true; // dynamic internal state
+			if (!variable.module && variable.writable && variable.export_name) return true; // writable props
+		});
+	}
+
 	getPrecedence() {
 		return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
 	}
@@ -230,7 +248,7 @@ export default class Expression {
 					const { name, nodes } = flattenReference(node);
 
 					if (scope.has(name)) return;
-					if (globalWhitelist.has(name) && component.declarations.indexOf(name) === -1) return;
+					if (globalWhitelist.has(name) && !component.var_lookup.has(name)) return;
 
 					if (function_expression) {
 						if (template_scope.names.has(name)) {
@@ -241,7 +259,7 @@ export default class Expression {
 							});
 						} else {
 							dependencies.add(name);
-							component.template_references.add(name);
+							component.add_reference(name);
 						}
 					} else if (!is_synthetic && isContextual(component, template_scope, name)) {
 						code.prependRight(node.start, key === 'key' && parent.shorthand
@@ -276,7 +294,9 @@ export default class Expression {
 						} else {
 							names.forEach(name => {
 								if (scope.declarations.has(name)) return;
-								if (component.imported_declarations.has(name)) return;
+
+								const variable = component.var_lookup.get(name);
+								if (variable && variable.hoistable) return;
 
 								pending_assignments.add(name);
 							});
@@ -285,7 +305,9 @@ export default class Expression {
 						const { name } = getObject(node.argument);
 
 						if (scope.declarations.has(name)) return;
-						if (component.imported_declarations.has(name)) return;
+
+						const variable = component.var_lookup.get(name);
+						if (variable && variable.hoistable) return;
 
 						pending_assignments.add(name);
 					}
@@ -359,23 +381,38 @@ export default class Expression {
 						// we can hoist this out of the component completely
 						component.fully_hoisted.push(fn);
 						code.overwrite(node.start, node.end, name);
+
+						component.add_var({
+							name,
+							internal: true,
+							hoistable: true,
+							referenced: true
+						});
 					}
 
 					else if (contextual_dependencies.size === 0) {
 						// function can be hoisted inside the component init
 						component.partly_hoisted.push(fn);
-						component.declarations.push(name);
-						component.template_references.add(name);
 						code.overwrite(node.start, node.end, `ctx.${name}`);
+
+						component.add_var({
+							name,
+							internal: true,
+							referenced: true
+						});
 					}
 
 					else {
 						// we need a combo block/init recipe
 						component.partly_hoisted.push(fn);
-						component.declarations.push(name);
-						component.template_references.add(name);
 						code.overwrite(node.start, node.end, name);
 
+						component.add_var({
+							name,
+							internal: true,
+							referenced: true
+						});
+
 						declarations.push(deindent`
 							function ${name}(${original_params ? '...args' : ''}) {
 								return ctx.${name}(ctx${original_params ? ', ...args' : ''});
@@ -445,10 +482,10 @@ function isContextual(component: Component, scope: TemplateScope, name: string)
 	// if it's a name below root scope, it's contextual
 	if (!scope.isTopLevel(name)) return true;
 
+	const variable = component.var_lookup.get(name);
+
 	// hoistables, module declarations, and imports are non-contextual
-	if (component.hoistable_names.has(name)) return false;
-	if (component.module_scope && component.module_scope.declarations.has(name)) return false;
-	if (component.imported_declarations.has(name)) return false;
+	if (!variable || variable.hoistable) return false;
 
 	// assume contextual
 	return true;
diff --git a/src/compile/nodes/shared/TemplateScope.ts b/src/compile/nodes/shared/TemplateScope.ts
index df48cc57f9..70522c88ed 100644
--- a/src/compile/nodes/shared/TemplateScope.ts
+++ b/src/compile/nodes/shared/TemplateScope.ts
@@ -9,7 +9,6 @@ type NodeWithScope = EachBlock | ThenBlock | CatchBlock | InlineComponent | Elem
 export default class TemplateScope {
 	names: Set<string>;
 	dependenciesForName: Map<string, Set<string>>;
-	mutables: Set<string> = new Set();
 	owners: Map<string, NodeWithScope> = new Map();
 	parent?: TemplateScope;
 
@@ -31,29 +30,6 @@ export default class TemplateScope {
 		return child;
 	}
 
-	setMutable(name: string) {
-		if (this.names.has(name)) {
-			this.mutables.add(name);
-			if (this.parent && this.dependenciesForName.has(name)) this.dependenciesForName.get(name).forEach(dep => this.parent.setMutable(dep));
-		} else if (this.parent) this.parent.setMutable(name);
-		else this.mutables.add(name);
-	}
-
-	containsMutable(names: Iterable<string>) {
-		for (const name of names) {
-			const owner = this.getOwner(name);
-			const is_let = owner && (owner.type === 'InlineComponent' || owner.type === 'Element');
-			if (is_let) return true;
-
-			if (name[0] === '$') return true;
-			if (this.mutables.has(name)) return true;
-			else if (this.dependenciesForName.has(name) && this.containsMutable(this.dependenciesForName.get(name))) return true;
-		}
-
-		if (this.parent) return this.parent.containsMutable(names);
-		else return false;
-	}
-
 	isTopLevel(name: string) {
 		return !this.parent || !this.names.has(name) && this.parent.isTopLevel(name);
 	}
@@ -61,4 +37,9 @@ export default class TemplateScope {
 	getOwner(name: string): NodeWithScope {
 		return this.owners.get(name) || (this.parent && this.parent.getOwner(name));
 	}
+
+	is_let(name: string) {
+		const owner = this.getOwner(name);
+		return owner && (owner.type === 'Element' || owner.type === 'InlineComponent');
+	}
 }
\ No newline at end of file
diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts
index c7d79fb5d2..8567725763 100644
--- a/src/compile/render-dom/index.ts
+++ b/src/compile/render-dom/index.ts
@@ -70,9 +70,10 @@ export default function dom(
 		options.css !== false
 	);
 
-	const props = component.props.filter(x => component.writable_declarations.has(x.name));
+	const props = component.vars.filter(variable => !variable.module && variable.export_name);
+	const writable_props = props.filter(variable => variable.writable);
 
-	const set = (component.meta.props || props.length > 0 || renderer.slots.size > 0)
+	const set = (component.meta.props || writable_props.length > 0 || renderer.slots.size > 0)
 		? deindent`
 			$$props => {
 				${component.meta.props && deindent`
@@ -80,8 +81,8 @@ export default function dom(
 				@assign(${component.meta.props}, $$props);
 				$$invalidate('${component.meta.props_object}', ${component.meta.props_object});
 				`}
-				${props.map(prop =>
-				`if ('${prop.as}' in $$props) $$invalidate('${prop.name}', ${prop.name} = $$props.${prop.as});`)}
+				${writable_props.map(prop =>
+				`if ('${prop.export_name}' in $$props) $$invalidate('${prop.name}', ${prop.name} = $$props.${prop.export_name});`)}
 				${renderer.slots.size > 0 &&
 				`if ('$$scope' in $$props) $$invalidate('$$scope', $$scope = $$props.$$scope);`}
 			}
@@ -93,32 +94,34 @@ export default function dom(
 	const not_equal = component.meta.immutable ? `@not_equal` : `@safe_not_equal`;
 	let dev_props_check;
 
-	component.props.forEach(x => {
-		if (component.imported_declarations.has(x.name) || component.hoistable_names.has(x.name)) {
+	props.forEach(x => {
+		const variable = component.var_lookup.get(x.name);
+
+		if (variable.hoistable) {
 			body.push(deindent`
-				get ${x.as}() {
+				get ${x.export_name}() {
 					return ${x.name};
 				}
 			`);
 		} else {
 			body.push(deindent`
-				get ${x.as}() {
+				get ${x.export_name}() {
 					return this.$$.ctx.${x.name};
 				}
 			`);
 		}
 
-		if (component.writable_declarations.has(x.as) && !renderer.readonly.has(x.as)) {
+		if (variable.writable && !renderer.readonly.has(x.export_name)) {
 			body.push(deindent`
-				set ${x.as}(${x.name}) {
+				set ${x.export_name}(${x.name}) {
 					this.$set({ ${x.name} });
 					@flush();
 				}
 			`);
 		} else if (component.options.dev) {
 			body.push(deindent`
-				set ${x.as}(value) {
-					throw new Error("<${component.tag}>: Cannot set read-only property '${x.as}'");
+				set ${x.export_name}(value) {
+					throw new Error("<${component.tag}>: Cannot set read-only property '${x.export_name}'");
 				}
 			`);
 		}
@@ -127,30 +130,28 @@ export default function dom(
 	if (component.options.dev) {
 		// TODO check no uunexpected props were passed, as well as
 		// checking that expected ones were passed
-		const expected = component.props
-			.map(x => x.name)
-			.filter(name => !component.initialised_declarations.has(name));
+		const expected = props.filter(prop => !prop.initialised);
 
 		if (expected.length) {
 			dev_props_check = deindent`
 				const { ctx } = this.$$;
 				const props = ${options.customElement ? `this.attributes` : `options.props || {}`};
-				${expected.map(name => deindent`
-				if (ctx.${name} === undefined && !('${name}' in props)) {
-					console.warn("<${component.tag}> was created without expected prop '${name}'");
+				${expected.map(prop => deindent`
+				if (ctx.${prop.name} === undefined && !('${prop.export_name}' in props)) {
+					console.warn("<${component.tag}> was created without expected prop '${prop.export_name}'");
 				}`)}
 			`;
 		}
 	}
 
 	// instrument assignments
-	if (component.instance_script) {
+	if (component.ast.instance) {
 		let scope = component.instance_scope;
 		let map = component.instance_scope_map;
 
 		let pending_assignments = new Set();
 
-		walk(component.instance_script.content, {
+		walk(component.ast.instance.content, {
 			enter: (node, parent) => {
 				if (map.has(node)) {
 					scope = map.get(node);
@@ -177,9 +178,11 @@ export default function dom(
 						code.overwrite(node.start, node.end, dirty.map(n => `$$invalidate('${n}', ${n})`).join('; '));
 					} else {
 						names.forEach(name => {
-							if (component.imported_declarations.has(name)) return;
 							if (scope.findOwner(name) !== component.instance_scope) return;
 
+							const variable = component.var_lookup.get(name);
+							if (variable && variable.hoistable) return;
+
 							pending_assignments.add(name);
 							component.has_reactive_assignments = true;
 						});
@@ -189,9 +192,11 @@ export default function dom(
 				else if (node.type === 'UpdateExpression') {
 					const { name } = getObject(node.argument);
 
-					if (component.imported_declarations.has(name)) return;
 					if (scope.findOwner(name) !== component.instance_scope) return;
 
+					const variable = component.var_lookup.get(name);
+					if (variable && variable.hoistable) return;
+
 					pending_assignments.add(name);
 					component.has_reactive_assignments = true;
 				}
@@ -238,7 +243,7 @@ export default function dom(
 	}
 
 	const args = ['$$self'];
-	if (component.props.length > 0 || component.has_reactive_assignments || renderer.slots.size > 0) {
+	if (props.length > 0 || component.has_reactive_assignments || renderer.slots.size > 0) {
 		args.push('$$props', '$$invalidate');
 	}
 
@@ -252,22 +257,19 @@ export default function dom(
 		${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')}
 	`);
 
-	const filtered_declarations = component.declarations.filter(name => {
-		if (component.hoistable_names.has(name)) return false;
-		if (component.imported_declarations.has(name)) return false;
-		if (component.props.find(p => p.as === name)) return true;
-		return component.template_references.has(name);
-	});
+	const filtered_declarations = component.vars.filter(variable => {
+		return (variable.referenced || variable.export_name) && !variable.hoistable;
+	}).map(variable => variable.name);
+
+	const filtered_props = props.filter(prop => {
+		const variable = component.var_lookup.get(prop.name);
 
-	const filtered_props = component.props.filter(prop => {
-		if (component.hoistable_names.has(prop.name)) return false;
-		if (component.imported_declarations.has(prop.name)) return false;
+		if (variable.hoistable) return false;
 		if (prop.name[0] === '$') return false;
 		return true;
 	});
 
-	const reactive_stores = Array.from(component.template_references).filter(n => n[0] === '$');
-	filtered_declarations.push(...reactive_stores);
+	const reactive_stores = component.vars.filter(variable => variable.name[0] === '$');
 
 	if (renderer.slots.size > 0) {
 		const arr = Array.from(renderer.slots);
@@ -283,7 +285,6 @@ export default function dom(
 		filtered_props.length > 0 ||
 		component.partly_hoisted.length > 0 ||
 		filtered_declarations.length > 0 ||
-		reactive_stores.length > 0 ||
 		component.reactive_declarations.length > 0
 	);
 
@@ -297,13 +298,13 @@ export default function dom(
 	});
 
 	const user_code = component.javascript || (
-		component.ast.js.length === 0 && filtered_props.length > 0
+		!component.ast.instance && !component.ast.module && filtered_props.length > 0
 			? `let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;`
 			: null
 	);
 
 	const reactive_store_subscriptions = reactive_stores.length > 0 && reactive_stores
-		.map(name => deindent`
+		.map(({ name }) => deindent`
 			let ${name};
 			${component.options.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}
 			$$self.$$.on_destroy.push(${name.slice(1)}.subscribe($$value => { ${name} = $$value; $$invalidate('${name}', ${name}); }));
@@ -354,7 +355,7 @@ export default function dom(
 							@insert(options.target, this, options.anchor);
 						}
 
-						${(component.props.length > 0 || component.meta.props) && deindent`
+						${(props.length > 0 || component.meta.props) && deindent`
 						if (options.props) {
 							this.$set(options.props);
 							@flush();
@@ -363,7 +364,7 @@ export default function dom(
 				}
 
 				static get observedAttributes() {
-					return ${JSON.stringify(component.props.map(x => x.as))};
+					return ${JSON.stringify(props.map(x => x.export_name))};
 				}
 
 				${body.length > 0 && body.join('\n\n')}
diff --git a/src/compile/render-dom/wrappers/AwaitBlock.ts b/src/compile/render-dom/wrappers/AwaitBlock.ts
index a5926fa8d7..e896510fa8 100644
--- a/src/compile/render-dom/wrappers/AwaitBlock.ts
+++ b/src/compile/render-dom/wrappers/AwaitBlock.ts
@@ -67,7 +67,7 @@ export default class AwaitBlockWrapper extends Wrapper {
 
 		this.cannotUseInnerHTML();
 
-		block.addDependencies(this.node.expression.dynamic_dependencies);
+		block.addDependencies(this.node.expression.dependencies);
 
 		let isDynamic = false;
 		let hasIntros = false;
@@ -181,9 +181,11 @@ export default class AwaitBlockWrapper extends Wrapper {
 		}
 
 		const conditions = [];
-		if (this.node.expression.dependencies.size > 0) {
+		const dependencies = this.node.expression.dynamic_dependencies();
+
+		if (dependencies.length > 0) {
 			conditions.push(
-				`(${[...this.node.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})`
+				`(${dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
 			);
 		}
 
diff --git a/src/compile/render-dom/wrappers/DebugTag.ts b/src/compile/render-dom/wrappers/DebugTag.ts
index cec40baaf7..294276a4ea 100644
--- a/src/compile/render-dom/wrappers/DebugTag.ts
+++ b/src/compile/render-dom/wrappers/DebugTag.ts
@@ -45,7 +45,7 @@ export default class DebugTagWrapper extends Wrapper {
 
 			const dependencies = new Set();
 			this.node.expressions.forEach(expression => {
-				addToSet(dependencies, expression.dynamic_dependencies);
+				addToSet(dependencies, expression.dependencies);
 			});
 
 			const condition = [...dependencies].map(d => `changed.${d}`).join(' || ');
diff --git a/src/compile/render-dom/wrappers/EachBlock.ts b/src/compile/render-dom/wrappers/EachBlock.ts
index f756a9ce6e..0fb1018ccf 100644
--- a/src/compile/render-dom/wrappers/EachBlock.ts
+++ b/src/compile/render-dom/wrappers/EachBlock.ts
@@ -74,8 +74,8 @@ export default class EachBlockWrapper extends Wrapper {
 		super(renderer, block, parent, node);
 		this.cannotUseInnerHTML();
 
-		const { dynamic_dependencies } = node.expression;
-		block.addDependencies(dynamic_dependencies);
+		const { dependencies } = node.expression;
+		block.addDependencies(dependencies);
 
 		this.block = block.child({
 			comment: createDebuggingComment(this.node, this.renderer.component),
diff --git a/src/compile/render-dom/wrappers/Element/Attribute.ts b/src/compile/render-dom/wrappers/Element/Attribute.ts
index dbb53b665f..ba4186f360 100644
--- a/src/compile/render-dom/wrappers/Element/Attribute.ts
+++ b/src/compile/render-dom/wrappers/Element/Attribute.ts
@@ -160,8 +160,8 @@ export default class AttributeWrapper {
 			}
 
 			// only add an update if mutations are involved (or it's a select?)
-			if (this.node.parent.scope.containsMutable(this.node.dependencies) || isSelectValueAttribute) {
-				const dependencies = Array.from(this.node.dependencies);
+			const dependencies = this.node.get_dependencies();
+			if (dependencies.length > 0 || isSelectValueAttribute) {
 				const changedCheck = (
 					(block.hasOutros ? `!#current || ` : '') +
 					dependencies.map(dependency => `changed.${dependency}`).join(' || ')
diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts
index 7604c16cc2..0dd41b6e62 100644
--- a/src/compile/render-dom/wrappers/Element/Binding.ts
+++ b/src/compile/render-dom/wrappers/Element/Binding.ts
@@ -37,14 +37,14 @@ export default class BindingWrapper {
 		this.node = node;
 		this.parent = parent;
 
-		const { dynamic_dependencies } = this.node.expression;
+		const { dependencies } = this.node.expression;
 
-		block.addDependencies(dynamic_dependencies);
+		block.addDependencies(dependencies);
 
 		// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
 		if (parent.node.name === 'select') {
-			parent.selectBindingDependencies = dynamic_dependencies;
-			dynamic_dependencies.forEach((prop: string) => {
+			parent.selectBindingDependencies = dependencies;
+			dependencies.forEach((prop: string) => {
 				parent.renderer.component.indirectDependencies.set(prop, new Set());
 			});
 		}
@@ -106,7 +106,7 @@ export default class BindingWrapper {
 
 		let updateConditions: string[] = this.needsLock ? [`!${lock}`] : [];
 
-		const dependencyArray = [...this.node.expression.dynamic_dependencies]
+		const dependencyArray = [...this.node.expression.dependencies]
 
 		if (dependencyArray.length === 1) {
 			updateConditions.push(`changed.${dependencyArray[0]}`)
diff --git a/src/compile/render-dom/wrappers/Element/StyleAttribute.ts b/src/compile/render-dom/wrappers/Element/StyleAttribute.ts
index 7af2a8dc79..2ec4a1c98e 100644
--- a/src/compile/render-dom/wrappers/Element/StyleAttribute.ts
+++ b/src/compile/render-dom/wrappers/Element/StyleAttribute.ts
@@ -35,7 +35,7 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
 							} else {
 								const snippet = chunk.render();
 
-								addToSet(propDependencies, chunk.dynamic_dependencies);
+								addToSet(propDependencies, chunk.dependencies);
 
 								return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
 							}
diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts
index 97d0a90459..77cb4bbd9f 100644
--- a/src/compile/render-dom/wrappers/Element/index.ts
+++ b/src/compile/render-dom/wrappers/Element/index.ts
@@ -132,7 +132,7 @@ export default class ElementWrapper extends Wrapper {
 					const name = attribute.getStaticValue();
 
 					if (!(owner as InlineComponentWrapper).slots.has(name)) {
-						const child_block = block.child({
+						const child_block = block.parent.child({
 							comment: createDebuggingComment(node, this.renderer.component),
 							name: this.renderer.component.getUniqueName(`create_${sanitize(name)}_slot`)
 						});
@@ -141,6 +141,7 @@ export default class ElementWrapper extends Wrapper {
 
 						(owner as InlineComponentWrapper).slots.set(name, {
 							block: child_block,
+							scope: this.node.scope,
 							fn
 						});
 						this.renderer.blocks.push(child_block);
@@ -173,13 +174,13 @@ export default class ElementWrapper extends Wrapper {
 		// add directive and handler dependencies
 		[node.animation, node.outro, ...node.actions, ...node.classes].forEach(directive => {
 			if (directive && directive.expression) {
-				block.addDependencies(directive.expression.dynamic_dependencies);
+				block.addDependencies(directive.expression.dependencies);
 			}
 		});
 
 		node.handlers.forEach(handler => {
 			if (handler.expression) {
-				block.addDependencies(handler.expression.dynamic_dependencies);
+				block.addDependencies(handler.expression.dependencies);
 			}
 		});
 
@@ -404,8 +405,12 @@ export default class ElementWrapper extends Wrapper {
 
 		groups.forEach(group => {
 			const handler = renderer.component.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
-			renderer.component.declarations.push(handler);
-			renderer.component.template_references.add(handler);
+
+			renderer.component.add_var({
+				name: handler,
+				internal: true,
+				referenced: true
+			});
 
 			// TODO figure out how to handle locks
 			const needsLock = group.bindings.some(binding => binding.needsLock);
@@ -505,8 +510,12 @@ export default class ElementWrapper extends Wrapper {
 		const this_binding = this.bindings.find(b => b.node.name === 'this');
 		if (this_binding) {
 			const name = renderer.component.getUniqueName(`${this.var}_binding`);
-			renderer.component.declarations.push(name);
-			renderer.component.template_references.add(name);
+
+			renderer.component.add_var({
+				name,
+				internal: true,
+				referenced: true
+			});
 
 			const { handler, object } = this_binding;
 
diff --git a/src/compile/render-dom/wrappers/IfBlock.ts b/src/compile/render-dom/wrappers/IfBlock.ts
index c3a8157dbe..ce2d5fda0f 100644
--- a/src/compile/render-dom/wrappers/IfBlock.ts
+++ b/src/compile/render-dom/wrappers/IfBlock.ts
@@ -87,7 +87,7 @@ export default class IfBlockWrapper extends Wrapper {
 			this.branches.push(branch);
 
 			blocks.push(branch.block);
-			block.addDependencies(node.expression.dynamic_dependencies);
+			block.addDependencies(node.expression.dependencies);
 
 			if (branch.block.dependencies.size > 0) {
 				isDynamic = true;
diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts
index 75bff825c6..f4dfe6f123 100644
--- a/src/compile/render-dom/wrappers/InlineComponent/index.ts
+++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts
@@ -15,10 +15,11 @@ import createDebuggingComment from '../../../../utils/createDebuggingComment';
 import sanitize from '../../../../utils/sanitize';
 import { get_context_merger } from '../shared/get_context_merger';
 import EachBlock from '../../../nodes/EachBlock';
+import TemplateScope from '../../../nodes/shared/TemplateScope';
 
 export default class InlineComponentWrapper extends Wrapper {
 	var: string;
-	slots: Map<string, { block: Block, fn?: string }> = new Map();
+	slots: Map<string, { block: Block, scope: TemplateScope, fn?: string }> = new Map();
 	node: InlineComponent;
 	fragment: FragmentWrapper;
 
@@ -35,7 +36,7 @@ export default class InlineComponentWrapper extends Wrapper {
 		this.cannotUseInnerHTML();
 
 		if (this.node.expression) {
-			block.addDependencies(this.node.expression.dynamic_dependencies);
+			block.addDependencies(this.node.expression.dependencies);
 		}
 
 		this.node.attributes.forEach(attr => {
@@ -52,12 +53,12 @@ export default class InlineComponentWrapper extends Wrapper {
 				(eachBlock as EachBlock).has_binding = true;
 			}
 
-			block.addDependencies(binding.expression.dynamic_dependencies);
+			block.addDependencies(binding.expression.dependencies);
 		});
 
 		this.node.handlers.forEach(handler => {
 			if (handler.expression) {
-				block.addDependencies(handler.expression.dynamic_dependencies);
+				block.addDependencies(handler.expression.dependencies);
 			}
 		});
 
@@ -79,6 +80,7 @@ export default class InlineComponentWrapper extends Wrapper {
 
 			this.slots.set('default', {
 				block: default_slot,
+				scope: this.node.scope,
 				fn
 			});
 			this.fragment = new FragmentWrapper(renderer, default_slot, node.children, this, stripWhitespace, nextSibling);
@@ -147,9 +149,14 @@ export default class InlineComponentWrapper extends Wrapper {
 		const fragment_dependencies = new Set();
 		this.slots.forEach(slot => {
 			slot.block.dependencies.forEach(name => {
-				if (renderer.component.mutable_props.has(name)) {
-					fragment_dependencies.add(name);
-				}
+				const is_let = slot.scope.is_let(name);
+				const variable = renderer.component.var_lookup.get(name);
+
+				if (is_let) fragment_dependencies.add(name);
+
+				if (!variable) return;
+				if (variable.mutated || variable.reassigned) fragment_dependencies.add(name);
+				if (!variable.module && variable.writable && variable.export_name) fragment_dependencies.add(name);
 			});
 		});
 
@@ -233,8 +240,12 @@ export default class InlineComponentWrapper extends Wrapper {
 
 			if (binding.name === 'this') {
 				const fn = component.getUniqueName(`${this.var}_binding`);
-				component.declarations.push(fn);
-				component.template_references.add(fn);
+
+				component.add_var({
+					name: fn,
+					internal: true,
+					referenced: true
+				});
 
 				let lhs;
 				let object;
@@ -264,8 +275,12 @@ export default class InlineComponentWrapper extends Wrapper {
 			}
 
 			const name = component.getUniqueName(`${this.var}_${binding.name}_binding`);
-			component.declarations.push(name);
-			component.template_references.add(name);
+
+			component.add_var({
+				name,
+				internal: true,
+				referenced: true
+			});
 
 			const updating = block.getUniqueName(`updating_${binding.name}`);
 			block.addVariable(updating);
@@ -279,7 +294,7 @@ export default class InlineComponentWrapper extends Wrapper {
 			);
 
 			updates.push(deindent`
-				if (!${updating} && ${[...binding.expression.dynamic_dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
+				if (!${updating} && ${[...binding.expression.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
 					${name_changes}${quotePropIfNecessary(binding.name)} = ${snippet};
 				}
 			`);
diff --git a/src/compile/render-dom/wrappers/Title.ts b/src/compile/render-dom/wrappers/Title.ts
index 733e4c845d..d82d950639 100644
--- a/src/compile/render-dom/wrappers/Title.ts
+++ b/src/compile/render-dom/wrappers/Title.ts
@@ -34,7 +34,7 @@ export default class TitleWrapper extends Wrapper {
 				// single {tag} — may be a non-string
 				const { expression } = this.node.children[0];
 				value = expression.render(block);
-				addToSet(allDependencies, expression.dynamic_dependencies);
+				addToSet(allDependencies, expression.dependencies);
 			} else {
 				// '{foo} {bar}' — treat as string concatenation
 				value =
@@ -46,7 +46,7 @@ export default class TitleWrapper extends Wrapper {
 							} else {
 								const snippet = chunk.expression.render(block);
 
-								chunk.expression.dynamic_dependencies.forEach(d => {
+								chunk.expression.dependencies.forEach(d => {
 									allDependencies.add(d);
 								});
 
diff --git a/src/compile/render-dom/wrappers/Window.ts b/src/compile/render-dom/wrappers/Window.ts
index c986d3726f..18a15dcd77 100644
--- a/src/compile/render-dom/wrappers/Window.ts
+++ b/src/compile/render-dom/wrappers/Window.ts
@@ -118,16 +118,18 @@ export default class WindowWrapper extends Wrapper {
 				`);
 			}
 
-			component.declarations.push(handler_name);
-			component.template_references.add(handler_name);
+			component.add_var({
+				name: handler_name,
+				internal: true,
+				referenced: true
+			});
+
 			component.partly_hoisted.push(deindent`
 				function ${handler_name}() {
 					${props.map(prop => `${prop.name} = window.${prop.value}; $$invalidate('${prop.name}', ${prop.name});`)}
 				}
 			`);
 
-
-
 			block.builders.init.addBlock(deindent`
 				@add_render_callback(ctx.${handler_name});
 			`);
diff --git a/src/compile/render-dom/wrappers/shared/Tag.ts b/src/compile/render-dom/wrappers/shared/Tag.ts
index 475c0d8a70..8e72929acd 100644
--- a/src/compile/render-dom/wrappers/shared/Tag.ts
+++ b/src/compile/render-dom/wrappers/shared/Tag.ts
@@ -11,14 +11,14 @@ export default class Tag extends Wrapper {
 		super(renderer, block, parent, node);
 		this.cannotUseInnerHTML();
 
-		block.addDependencies(node.expression.dynamic_dependencies);
+		block.addDependencies(node.expression.dependencies);
 	}
 
 	renameThisMethod(
 		block: Block,
 		update: ((value: string) => string)
 	) {
-		const dependencies = this.node.expression.dynamic_dependencies;
+		const dependencies = this.node.expression.dynamic_dependencies();
 		const snippet = this.node.expression.render(block);
 
 		const value = this.node.shouldCache && block.getUniqueName(`${this.var}_value`);
@@ -26,27 +26,22 @@ export default class Tag extends Wrapper {
 
 		if (this.node.shouldCache) block.addVariable(value, snippet);
 
-		if (dependencies.size) {
+		if (dependencies.length > 0) {
 			const changedCheck = (
 				(block.hasOutros ? `!#current || ` : '') +
-				[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
+				dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
 			);
 
 			const updateCachedValue = `${value} !== (${value} = ${snippet})`;
 
 			const condition =this.node.shouldCache
-				? dependencies.size > 0
-					? `(${changedCheck}) && ${updateCachedValue}`
-					: updateCachedValue
+				? `(${changedCheck}) && ${updateCachedValue}`
 				: changedCheck;
 
-			// only update if there's a mutation involved
-			if (this.node.expression.template_scope.containsMutable(dependencies)) {
-				block.builders.update.addConditional(
-					condition,
-					update(content)
-				);
-			}
+			block.builders.update.addConditional(
+				condition,
+				update(content)
+			);
 		}
 
 		return { init: content };
diff --git a/src/compile/render-dom/wrappers/shared/addActions.ts b/src/compile/render-dom/wrappers/shared/addActions.ts
index 48c2287f87..68f203e09a 100644
--- a/src/compile/render-dom/wrappers/shared/addActions.ts
+++ b/src/compile/render-dom/wrappers/shared/addActions.ts
@@ -1,4 +1,3 @@
-import Renderer from '../../Renderer';
 import Block from '../../Block';
 import Action from '../../../nodes/Action';
 import Component from '../../../Component';
@@ -15,7 +14,7 @@ export default function addActions(
 
 		if (expression) {
 			snippet = expression.render(block);
-			dependencies = expression.dynamic_dependencies;
+			dependencies = expression.dynamic_dependencies();
 		}
 
 		const name = block.getUniqueName(
@@ -23,20 +22,17 @@ export default function addActions(
 		);
 
 		block.addVariable(name);
-		const fn = component.imported_declarations.has(action.name) || component.hoistable_names.has(action.name)
-			? action.name
-			: `ctx.${action.name}`;
 
-		component.template_references.add(action.name);
+		const fn = component.qualify(action.name);
 
 		block.builders.mount.addLine(
 			`${name} = ${fn}.call(null, ${target}${snippet ? `, ${snippet}` : ''}) || {};`
 		);
 
-		if (dependencies && dependencies.size > 0) {
+		if (dependencies && dependencies.length > 0) {
 			let conditional = `typeof ${name}.update === 'function' && `;
-			const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
-			conditional += dependencies.size > 1 ? `(${deps})` : deps;
+			const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
+			conditional += dependencies.length > 1 ? `(${deps})` : deps;
 
 			block.builders.update.addConditional(
 				conditional,
diff --git a/src/compile/render-ssr/index.ts b/src/compile/render-ssr/index.ts
index 1289b9ef88..351a36570a 100644
--- a/src/compile/render-ssr/index.ts
+++ b/src/compile/render-ssr/index.ts
@@ -24,17 +24,18 @@ export default function ssr(
 
 	let user_code;
 
+	// TODO remove this, just use component.symbols everywhere
+	const props = component.vars.filter(variable => !variable.module && variable.export_name);
+
 	if (component.javascript) {
 		component.rewrite_props();
 		user_code = component.javascript;
-	} else if (component.ast.js.length === 0 && component.props.length > 0) {
-		const props = component.props.map(prop => prop.as).filter(name => name[0] !== '$');
-
-		user_code = `let { ${props.join(', ')} } = $$props;`
+	} else if (!component.ast.instance && !component.ast.module && props.length > 0) {
+		user_code = `let { ${props.map(prop => prop.export_name).join(', ')} } = $$props;`
 	}
 
-	const reactive_stores = Array.from(component.template_references).filter(n => n[0] === '$');
-	const reactive_store_values = reactive_stores.map(name => {
+	const reactive_stores = component.vars.filter(variable => variable.name[0] === '$');
+	const reactive_store_values = reactive_stores.map(({ name }) => {
 		const assignment = `const ${name} = @get_store_value(${name.slice(1)});`;
 
 		return component.options.dev
@@ -44,8 +45,8 @@ export default function ssr(
 
 	// TODO only do this for props with a default value
 	const parent_bindings = component.javascript
-		? component.props.map(prop => {
-			return `if ($$props.${prop.as} === void 0 && $$bindings.${prop.as} && ${prop.name} !== void 0) $$bindings.${prop.as}(${prop.name});`;
+		? props.map(prop => {
+			return `if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
 		})
 		: [];
 
diff --git a/src/interfaces.ts b/src/interfaces.ts
index ab2fcb1944..157ad56eb7 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -21,7 +21,8 @@ export interface Parser {
 export interface Ast {
 	html: Node;
 	css: Node;
-	js: Node;
+	instance: Node;
+	module: Node;
 }
 
 export interface Warning {
@@ -76,4 +77,22 @@ export interface CustomElementOptions {
 export interface AppendTarget {
 	slots: Record<string, string>;
 	slotStack: string[]
+}
+
+export interface Var {
+	name: string;
+	export_name?: string; // the `bar` in `export { foo as bar }`
+	injected?: boolean;
+	module?: boolean;
+	mutated?: boolean;
+	reassigned?: boolean;
+	referenced?: boolean;
+	writable?: boolean;
+
+	// used internally, but not exposed
+	global?: boolean;
+	implicit?: boolean; // logic-less template references
+	internal?: boolean; // event handlers, bindings
+	initialised?: boolean;
+	hoistable?: boolean;
 }
\ No newline at end of file
diff --git a/src/parse/acorn.ts b/src/parse/acorn.ts
index 2f0cd2ec20..cfb3cdbfbd 100644
--- a/src/parse/acorn.ts
+++ b/src/parse/acorn.ts
@@ -3,13 +3,13 @@ import dynamicImport from 'acorn-dynamic-import';
 
 const Parser = acorn.Parser.extend(dynamicImport);
 
-export const parse = (source: string, options: any) => Parser.parse(source, {
+export const parse = (source: string) => Parser.parse(source, {
 	sourceType: 'module',
 	ecmaVersion: 9,
 	preserveParens: true
 });
 
-export const parseExpressionAt = (source: string, index: number, options: any) => Parser.parseExpressionAt(source, index, {
+export const parseExpressionAt = (source: string, index: number) => Parser.parseExpressionAt(source, index, {
 	ecmaVersion: 9,
 	preserveParens: true
 });
\ No newline at end of file
diff --git a/src/parse/index.ts b/src/parse/index.ts
index bc37229ef9..4b1e0a225c 100644
--- a/src/parse/index.ts
+++ b/src/parse/index.ts
@@ -226,9 +226,27 @@ export default function parse(
 		}, parser.css[1].start);
 	}
 
+	const instance_scripts = parser.js.filter(script => script.context === 'default');
+	const module_scripts = parser.js.filter(script => script.context === 'module');
+
+	if (instance_scripts.length > 1) {
+		parser.error({
+			code: `invalid-script`,
+			message: `A component can only have one instance-level <script> element`
+		}, instance_scripts[1].start);
+	}
+
+	if (module_scripts.length > 1) {
+		parser.error({
+			code: `invalid-script`,
+			message: `A component can only have one <script context="module"> element`
+		}, module_scripts[1].start);
+	}
+
 	return {
 		html: parser.html,
-		css: parser.css,
-		js: parser.js,
+		css: parser.css[0],
+		instance: instance_scripts[0],
+		module: module_scripts[0]
 	};
 }
diff --git a/src/parse/read/script.ts b/src/parse/read/script.ts
index 11337db513..172e1a832b 100644
--- a/src/parse/read/script.ts
+++ b/src/parse/read/script.ts
@@ -5,6 +5,29 @@ import { Node } from '../../interfaces';
 
 const scriptClosingTag = '</script>';
 
+function get_context(parser: Parser, attributes: Node[], start: number) {
+	const context = attributes.find(attribute => attribute.name === 'context');
+	if (!context) return 'default';
+
+	if (context.value.length !== 1 || context.value[0].type !== 'Text') {
+		parser.error({
+			code: 'invalid-script',
+			message: `context attribute must be static`
+		}, start);
+	}
+
+	const value = context.value[0].data;
+
+	if (value !== 'module') {
+		parser.error({
+			code: `invalid-script`,
+			message: `If the context attribute is supplied, its value must be "module"`
+		}, context.start);
+	}
+
+	return value;
+}
+
 export default function readScript(parser: Parser, start: number, attributes: Node[]) {
 	const scriptStart = parser.index;
 	const scriptEnd = parser.template.indexOf(scriptClosingTag, scriptStart);
@@ -30,7 +53,7 @@ export default function readScript(parser: Parser, start: number, attributes: No
 	return {
 		start,
 		end: parser.index,
-		attributes,
+		context: get_context(parser, attributes, start),
 		content: ast,
 	};
 }
diff --git a/src/utils/annotateWithScopes.ts b/src/utils/annotateWithScopes.ts
index 96459e1ede..adf7619bed 100644
--- a/src/utils/annotateWithScopes.ts
+++ b/src/utils/annotateWithScopes.ts
@@ -52,6 +52,10 @@ export function createScopes(expression: Node) {
 		},
 	});
 
+	scope.declarations.forEach((node, name) => {
+		globals.delete(name);
+	});
+
 	return { map, scope, globals };
 }
 
@@ -60,7 +64,6 @@ export class Scope {
 	block: boolean;
 
 	declarations: Map<string, Node> = new Map();
-	writable_declarations: Set<string> = new Set();
 	initialised_declarations: Set<string> = new Set();
 
 	constructor(parent: Scope, block: boolean) {
@@ -72,13 +75,11 @@ export class Scope {
 		if (node.kind === 'var' && this.block && this.parent) {
 			this.parent.addDeclaration(node);
 		} else if (node.type === 'VariableDeclaration') {
-			const writable = node.kind !== 'const';
 			const initialised = !!node.init;
 
 			node.declarations.forEach((declarator: Node) => {
 				extractNames(declarator.id).forEach(name => {
 					this.declarations.set(name, node);
-					if (writable) this.writable_declarations.add(name);
 					if (initialised) this.initialised_declarations.add(name);
 				});
 			});
diff --git a/test/parser/index.js b/test/parser/index.js
index 8f20c77abd..900cc3ae05 100644
--- a/test/parser/index.js
+++ b/test/parser/index.js
@@ -31,7 +31,8 @@ describe('parse', () => {
 
 				assert.deepEqual(ast.html, expectedOutput.html);
 				assert.deepEqual(ast.css, expectedOutput.css);
-				assert.deepEqual(ast.js, expectedOutput.js);
+				assert.deepEqual(ast.instance, expectedOutput.instance);
+				assert.deepEqual(ast.module, expectedOutput.module);
 			} catch (err) {
 				if (err.name !== 'ParseError') throw err;
 				if (!expectedError) throw err;
diff --git a/test/parser/samples/action-with-call/output.json b/test/parser/samples/action-with-call/output.json
index 521599544b..d6aaa72892 100644
--- a/test/parser/samples/action-with-call/output.json
+++ b/test/parser/samples/action-with-call/output.json
@@ -42,6 +42,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/action-with-identifier/output.json b/test/parser/samples/action-with-identifier/output.json
index 800ab800be..d58b9097b7 100644
--- a/test/parser/samples/action-with-identifier/output.json
+++ b/test/parser/samples/action-with-identifier/output.json
@@ -28,6 +28,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/action-with-literal/output.json b/test/parser/samples/action-with-literal/output.json
index 0705cd9e39..4a6f596d10 100644
--- a/test/parser/samples/action-with-literal/output.json
+++ b/test/parser/samples/action-with-literal/output.json
@@ -29,6 +29,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/action/output.json b/test/parser/samples/action/output.json
index 68afd79d23..2f553b5efa 100644
--- a/test/parser/samples/action/output.json
+++ b/test/parser/samples/action/output.json
@@ -23,6 +23,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/animation/output.json b/test/parser/samples/animation/output.json
index 585ab3ff1c..8332b3ad04 100644
--- a/test/parser/samples/animation/output.json
+++ b/test/parser/samples/animation/output.json
@@ -55,6 +55,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-containing-solidus/output.json b/test/parser/samples/attribute-containing-solidus/output.json
index 920a9420c8..95372bd77d 100644
--- a/test/parser/samples/attribute-containing-solidus/output.json
+++ b/test/parser/samples/attribute-containing-solidus/output.json
@@ -36,6 +36,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-dynamic-boolean/output.json b/test/parser/samples/attribute-dynamic-boolean/output.json
index e50640f279..81a19f49b9 100644
--- a/test/parser/samples/attribute-dynamic-boolean/output.json
+++ b/test/parser/samples/attribute-dynamic-boolean/output.json
@@ -34,6 +34,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-dynamic-reserved/output.json b/test/parser/samples/attribute-dynamic-reserved/output.json
index 042712b872..3a830d448f 100644
--- a/test/parser/samples/attribute-dynamic-reserved/output.json
+++ b/test/parser/samples/attribute-dynamic-reserved/output.json
@@ -34,6 +34,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-dynamic/output.json b/test/parser/samples/attribute-dynamic/output.json
index f3099f6e62..50d8ec60a5 100644
--- a/test/parser/samples/attribute-dynamic/output.json
+++ b/test/parser/samples/attribute-dynamic/output.json
@@ -58,6 +58,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-escaped/output.json b/test/parser/samples/attribute-escaped/output.json
index 979b54d713..6a4143e674 100644
--- a/test/parser/samples/attribute-escaped/output.json
+++ b/test/parser/samples/attribute-escaped/output.json
@@ -29,6 +29,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-multiple/output.json b/test/parser/samples/attribute-multiple/output.json
index 5864363cdf..668409c0ef 100644
--- a/test/parser/samples/attribute-multiple/output.json
+++ b/test/parser/samples/attribute-multiple/output.json
@@ -43,6 +43,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-shorthand/output.json b/test/parser/samples/attribute-shorthand/output.json
index 69f9329ed1..08ddf5eda9 100644
--- a/test/parser/samples/attribute-shorthand/output.json
+++ b/test/parser/samples/attribute-shorthand/output.json
@@ -34,6 +34,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-static-boolean/output.json b/test/parser/samples/attribute-static-boolean/output.json
index 79ea9e9d11..f63b5734e0 100644
--- a/test/parser/samples/attribute-static-boolean/output.json
+++ b/test/parser/samples/attribute-static-boolean/output.json
@@ -22,6 +22,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-static/output.json b/test/parser/samples/attribute-static/output.json
index cb0d7684da..3185e48736 100644
--- a/test/parser/samples/attribute-static/output.json
+++ b/test/parser/samples/attribute-static/output.json
@@ -29,6 +29,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/attribute-unquoted/output.json b/test/parser/samples/attribute-unquoted/output.json
index 940ac5b01d..c7556f2eba 100644
--- a/test/parser/samples/attribute-unquoted/output.json
+++ b/test/parser/samples/attribute-unquoted/output.json
@@ -29,6 +29,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json
index 1ab557f99d..21fc13eff9 100644
--- a/test/parser/samples/await-then-catch/output.json
+++ b/test/parser/samples/await-then-catch/output.json
@@ -155,6 +155,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/binding-shorthand/output.json b/test/parser/samples/binding-shorthand/output.json
index c80fc5f7f2..3be1db50b4 100644
--- a/test/parser/samples/binding-shorthand/output.json
+++ b/test/parser/samples/binding-shorthand/output.json
@@ -28,6 +28,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/binding/output.json b/test/parser/samples/binding/output.json
index 031d291fa7..5dc173c2a2 100644
--- a/test/parser/samples/binding/output.json
+++ b/test/parser/samples/binding/output.json
@@ -28,6 +28,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/comment/output.json b/test/parser/samples/comment/output.json
index 10d85bf422..89295c188a 100644
--- a/test/parser/samples/comment/output.json
+++ b/test/parser/samples/comment/output.json
@@ -12,6 +12,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/component-dynamic/output.json b/test/parser/samples/component-dynamic/output.json
index 9b08732c38..c2e4e3ee79 100644
--- a/test/parser/samples/component-dynamic/output.json
+++ b/test/parser/samples/component-dynamic/output.json
@@ -37,6 +37,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/convert-entities-in-element/output.json b/test/parser/samples/convert-entities-in-element/output.json
index f481345a02..fb0f5b288e 100644
--- a/test/parser/samples/convert-entities-in-element/output.json
+++ b/test/parser/samples/convert-entities-in-element/output.json
@@ -21,6 +21,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/convert-entities/output.json b/test/parser/samples/convert-entities/output.json
index b3e66a9007..ca1c1356f8 100644
--- a/test/parser/samples/convert-entities/output.json
+++ b/test/parser/samples/convert-entities/output.json
@@ -12,6 +12,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/css/output.json b/test/parser/samples/css/output.json
index e91c31a71a..3127e01c71 100644
--- a/test/parser/samples/css/output.json
+++ b/test/parser/samples/css/output.json
@@ -27,71 +27,70 @@
 			}
 		]
 	},
-	"css": [
-		{
-			"start": 16,
-			"end": 56,
-			"attributes": [],
-			"children": [
-				{
-					"type": "Rule",
-					"selector": {
-						"type": "SelectorList",
-						"children": [
-							{
-								"type": "Selector",
+	"css": {
+		"start": 16,
+		"end": 56,
+		"attributes": [],
+		"children": [
+			{
+				"type": "Rule",
+				"selector": {
+					"type": "SelectorList",
+					"children": [
+						{
+							"type": "Selector",
+							"children": [
+								{
+									"type": "TypeSelector",
+									"name": "div",
+									"start": 25,
+									"end": 28
+								}
+							],
+							"start": 25,
+							"end": 28
+						}
+					],
+					"start": 25,
+					"end": 28
+				},
+				"block": {
+					"type": "Block",
+					"children": [
+						{
+							"type": "Declaration",
+							"important": false,
+							"property": "color",
+							"value": {
+								"type": "Value",
 								"children": [
 									{
-										"type": "TypeSelector",
-										"name": "div",
-										"start": 25,
-										"end": 28
+										"type": "Identifier",
+										"name": "red",
+										"start": 40,
+										"end": 43
 									}
 								],
-								"start": 25,
-								"end": 28
-							}
-						],
-						"start": 25,
-						"end": 28
-					},
-					"block": {
-						"type": "Block",
-						"children": [
-							{
-								"type": "Declaration",
-								"important": false,
-								"property": "color",
-								"value": {
-									"type": "Value",
-									"children": [
-										{
-											"type": "Identifier",
-											"name": "red",
-											"start": 40,
-											"end": 43
-										}
-									],
-									"start": 39,
-									"end": 43
-								},
-								"start": 33,
+								"start": 39,
 								"end": 43
-							}
-						],
-						"start": 29,
-						"end": 47
-					},
-					"start": 25,
+							},
+							"start": 33,
+							"end": 43
+						}
+					],
+					"start": 29,
 					"end": 47
-				}
-			],
-			"content": {
-				"start": 23,
-				"end": 48,
-				"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
+				},
+				"start": 25,
+				"end": 47
 			}
+		],
+		"content": {
+			"start": 23,
+			"end": 48,
+			"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
 		}
-	],
-	"js": []
+	},
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/dynamic-import/output.json b/test/parser/samples/dynamic-import/output.json
index 55c1bdf756..c0d4a45e0d 100644
--- a/test/parser/samples/dynamic-import/output.json
+++ b/test/parser/samples/dynamic-import/output.json
@@ -5,201 +5,199 @@
 		"type": "Fragment",
 		"children": []
 	},
-	"css": [],
-	"js": [
-		{
-			"start": 0,
-			"end": 146,
-			"attributes": [],
-			"content": {
-				"type": "Program",
-				"start": 8,
-				"end": 137,
-				"body": [
-					{
-						"type": "ImportDeclaration",
-						"start": 10,
-						"end": 43,
-						"specifiers": [
-							{
-								"type": "ImportSpecifier",
+	"css": null,
+	"instance": {
+		"start": 0,
+		"end": 146,
+		"context": "default",
+		"content": {
+			"type": "Program",
+			"start": 8,
+			"end": 137,
+			"body": [
+				{
+					"type": "ImportDeclaration",
+					"start": 10,
+					"end": 43,
+					"specifiers": [
+						{
+							"type": "ImportSpecifier",
+							"start": 19,
+							"end": 26,
+							"imported": {
+								"type": "Identifier",
 								"start": 19,
 								"end": 26,
-								"imported": {
-									"type": "Identifier",
-									"start": 19,
-									"end": 26,
-									"name": "onMount"
-								},
-								"local": {
-									"type": "Identifier",
-									"start": 19,
-									"end": 26,
-									"name": "onMount"
-								}
+								"name": "onMount"
+							},
+							"local": {
+								"type": "Identifier",
+								"start": 19,
+								"end": 26,
+								"name": "onMount"
 							}
-						],
-						"source": {
-							"type": "Literal",
-							"start": 34,
-							"end": 42,
-							"value": "svelte",
-							"raw": "'svelte'"
 						}
-					},
-					{
-						"type": "ExpressionStatement",
+					],
+					"source": {
+						"type": "Literal",
+						"start": 34,
+						"end": 42,
+						"value": "svelte",
+						"raw": "'svelte'"
+					}
+				},
+				{
+					"type": "ExpressionStatement",
+					"start": 46,
+					"end": 136,
+					"expression": {
+						"type": "CallExpression",
 						"start": 46,
-						"end": 136,
-						"expression": {
-							"type": "CallExpression",
+						"end": 135,
+						"callee": {
+							"type": "Identifier",
 							"start": 46,
-							"end": 135,
-							"callee": {
-								"type": "Identifier",
-								"start": 46,
-								"end": 53,
-								"name": "onMount"
-							},
-							"arguments": [
-								{
-									"type": "ArrowFunctionExpression",
-									"start": 54,
+							"end": 53,
+							"name": "onMount"
+						},
+						"arguments": [
+							{
+								"type": "ArrowFunctionExpression",
+								"start": 54,
+								"end": 134,
+								"id": null,
+								"generator": false,
+								"expression": false,
+								"async": false,
+								"params": [],
+								"body": {
+									"type": "BlockStatement",
+									"start": 60,
 									"end": 134,
-									"id": null,
-									"generator": false,
-									"expression": false,
-									"async": false,
-									"params": [],
-									"body": {
-										"type": "BlockStatement",
-										"start": 60,
-										"end": 134,
-										"body": [
-											{
-												"type": "ExpressionStatement",
+									"body": [
+										{
+											"type": "ExpressionStatement",
+											"start": 64,
+											"end": 131,
+											"expression": {
+												"type": "CallExpression",
 												"start": 64,
-												"end": 131,
-												"expression": {
-													"type": "CallExpression",
+												"end": 130,
+												"callee": {
+													"type": "MemberExpression",
 													"start": 64,
-													"end": 130,
-													"callee": {
-														"type": "MemberExpression",
+													"end": 87,
+													"object": {
+														"type": "CallExpression",
 														"start": 64,
-														"end": 87,
-														"object": {
-															"type": "CallExpression",
+														"end": 82,
+														"callee": {
+															"type": "Import",
 															"start": 64,
-															"end": 82,
-															"callee": {
-																"type": "Import",
-																"start": 64,
-																"end": 70
-															},
-															"arguments": [
-																{
-																	"type": "Literal",
-																	"start": 71,
-																	"end": 81,
-																	"value": "./foo.js",
-																	"raw": "'./foo.js'"
-																}
-															]
-														},
-														"property": {
-															"type": "Identifier",
-															"start": 83,
-															"end": 87,
-															"name": "then"
+															"end": 70
 														},
-														"computed": false
+														"arguments": [
+															{
+																"type": "Literal",
+																"start": 71,
+																"end": 81,
+																"value": "./foo.js",
+																"raw": "'./foo.js'"
+															}
+														]
+													},
+													"property": {
+														"type": "Identifier",
+														"start": 83,
+														"end": 87,
+														"name": "then"
 													},
-													"arguments": [
-														{
-															"type": "ArrowFunctionExpression",
-															"start": 88,
+													"computed": false
+												},
+												"arguments": [
+													{
+														"type": "ArrowFunctionExpression",
+														"start": 88,
+														"end": 129,
+														"id": null,
+														"generator": false,
+														"expression": false,
+														"async": false,
+														"params": [
+															{
+																"type": "Identifier",
+																"start": 88,
+																"end": 91,
+																"name": "foo"
+															}
+														],
+														"body": {
+															"type": "BlockStatement",
+															"start": 95,
 															"end": 129,
-															"id": null,
-															"generator": false,
-															"expression": false,
-															"async": false,
-															"params": [
+															"body": [
 																{
-																	"type": "Identifier",
-																	"start": 88,
-																	"end": 91,
-																	"name": "foo"
-																}
-															],
-															"body": {
-																"type": "BlockStatement",
-																"start": 95,
-																"end": 129,
-																"body": [
-																	{
-																		"type": "ExpressionStatement",
+																	"type": "ExpressionStatement",
+																	"start": 100,
+																	"end": 125,
+																	"expression": {
+																		"type": "CallExpression",
 																		"start": 100,
-																		"end": 125,
-																		"expression": {
-																			"type": "CallExpression",
+																		"end": 124,
+																		"callee": {
+																			"type": "MemberExpression",
 																			"start": 100,
-																			"end": 124,
-																			"callee": {
-																				"type": "MemberExpression",
+																			"end": 111,
+																			"object": {
+																				"type": "Identifier",
 																				"start": 100,
+																				"end": 107,
+																				"name": "console"
+																			},
+																			"property": {
+																				"type": "Identifier",
+																				"start": 108,
 																				"end": 111,
+																				"name": "log"
+																			},
+																			"computed": false
+																		},
+																		"arguments": [
+																			{
+																				"type": "MemberExpression",
+																				"start": 112,
+																				"end": 123,
 																				"object": {
 																					"type": "Identifier",
-																					"start": 100,
-																					"end": 107,
-																					"name": "console"
+																					"start": 112,
+																					"end": 115,
+																					"name": "foo"
 																				},
 																				"property": {
 																					"type": "Identifier",
-																					"start": 108,
-																					"end": 111,
-																					"name": "log"
+																					"start": 116,
+																					"end": 123,
+																					"name": "default"
 																				},
 																				"computed": false
-																			},
-																			"arguments": [
-																				{
-																					"type": "MemberExpression",
-																					"start": 112,
-																					"end": 123,
-																					"object": {
-																						"type": "Identifier",
-																						"start": 112,
-																						"end": 115,
-																						"name": "foo"
-																					},
-																					"property": {
-																						"type": "Identifier",
-																						"start": 116,
-																						"end": 123,
-																						"name": "default"
-																					},
-																					"computed": false
-																				}
-																			]
-																		}
+																			}
+																		]
 																	}
-																]
-															}
+																}
+															]
 														}
-													]
-												}
+													}
+												]
 											}
-										]
-									}
+										}
+									]
 								}
-							]
-						}
+							}
+						]
 					}
-				],
-				"sourceType": "module"
-			}
+				}
+			],
+			"sourceType": "module"
 		}
-	]
+	}
 }
\ No newline at end of file
diff --git a/test/parser/samples/each-block-destructured/output.json b/test/parser/samples/each-block-destructured/output.json
index fdca5f82a2..b9efc18906 100644
--- a/test/parser/samples/each-block-destructured/output.json
+++ b/test/parser/samples/each-block-destructured/output.json
@@ -75,6 +75,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/each-block-else/output.json b/test/parser/samples/each-block-else/output.json
index be0ac6d550..aefc8c2f39 100644
--- a/test/parser/samples/each-block-else/output.json
+++ b/test/parser/samples/each-block-else/output.json
@@ -67,6 +67,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/each-block-indexed/output.json b/test/parser/samples/each-block-indexed/output.json
index bda5f86a89..7518652468 100644
--- a/test/parser/samples/each-block-indexed/output.json
+++ b/test/parser/samples/each-block-indexed/output.json
@@ -63,6 +63,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/each-block-keyed/output.json b/test/parser/samples/each-block-keyed/output.json
index a00694fc4e..fe893bcdb9 100644
--- a/test/parser/samples/each-block-keyed/output.json
+++ b/test/parser/samples/each-block-keyed/output.json
@@ -63,6 +63,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/each-block/output.json b/test/parser/samples/each-block/output.json
index a24c32e9ad..c16a71ad5d 100644
--- a/test/parser/samples/each-block/output.json
+++ b/test/parser/samples/each-block/output.json
@@ -45,6 +45,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/element-with-mustache/output.json b/test/parser/samples/element-with-mustache/output.json
index a56cd27967..c8a386d681 100644
--- a/test/parser/samples/element-with-mustache/output.json
+++ b/test/parser/samples/element-with-mustache/output.json
@@ -38,6 +38,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/element-with-text/output.json b/test/parser/samples/element-with-text/output.json
index 1ae33df94f..70f6163c93 100644
--- a/test/parser/samples/element-with-text/output.json
+++ b/test/parser/samples/element-with-text/output.json
@@ -21,6 +21,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/elements/output.json b/test/parser/samples/elements/output.json
index 219799df3e..d216f7f5d8 100644
--- a/test/parser/samples/elements/output.json
+++ b/test/parser/samples/elements/output.json
@@ -22,6 +22,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/event-handler/output.json b/test/parser/samples/event-handler/output.json
index 740e43e48d..b1fe89fc2a 100644
--- a/test/parser/samples/event-handler/output.json
+++ b/test/parser/samples/event-handler/output.json
@@ -98,6 +98,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/if-block-else/output.json b/test/parser/samples/if-block-else/output.json
index 8e96c34efc..dedf2797c4 100644
--- a/test/parser/samples/if-block-else/output.json
+++ b/test/parser/samples/if-block-else/output.json
@@ -56,6 +56,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/if-block-elseif/output.json b/test/parser/samples/if-block-elseif/output.json
index 257eb1e7d2..426e1f7fbc 100644
--- a/test/parser/samples/if-block-elseif/output.json
+++ b/test/parser/samples/if-block-elseif/output.json
@@ -96,6 +96,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/if-block/output.json b/test/parser/samples/if-block/output.json
index ba734c0e56..d560824766 100644
--- a/test/parser/samples/if-block/output.json
+++ b/test/parser/samples/if-block/output.json
@@ -25,6 +25,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/implicitly-closed-li/output.json b/test/parser/samples/implicitly-closed-li/output.json
index 69def7a9a2..caf69d7109 100644
--- a/test/parser/samples/implicitly-closed-li/output.json
+++ b/test/parser/samples/implicitly-closed-li/output.json
@@ -66,6 +66,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/nbsp/output.json b/test/parser/samples/nbsp/output.json
index 7373bc9673..4fa318ce48 100644
--- a/test/parser/samples/nbsp/output.json
+++ b/test/parser/samples/nbsp/output.json
@@ -21,6 +21,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/raw-mustaches/output.json b/test/parser/samples/raw-mustaches/output.json
index 41dab885e0..1d92b21c85 100644
--- a/test/parser/samples/raw-mustaches/output.json
+++ b/test/parser/samples/raw-mustaches/output.json
@@ -55,6 +55,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/refs/output.json b/test/parser/samples/refs/output.json
index 3cd6c4d9fa..2cf5054304 100644
--- a/test/parser/samples/refs/output.json
+++ b/test/parser/samples/refs/output.json
@@ -28,6 +28,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/script-comment-only/output.json b/test/parser/samples/script-comment-only/output.json
index b04d38633d..95ca769b20 100644
--- a/test/parser/samples/script-comment-only/output.json
+++ b/test/parser/samples/script-comment-only/output.json
@@ -20,19 +20,16 @@
 			}
 		]
 	},
-	"css": [],
-	"js": [
-		{
-			"start": 0,
-			"end": 43,
-			"attributes": [],
-			"content": {
-				"type": "Program",
-				"start": 8,
-				"end": 34,
-				"body": [],
-				"sourceType": "module"
-			}
+	"instance": {
+		"start": 0,
+		"end": 43,
+		"context": "default",
+		"content": {
+			"type": "Program",
+			"start": 8,
+			"end": 34,
+			"body": [],
+			"sourceType": "module"
 		}
-	]
+	}
 }
\ No newline at end of file
diff --git a/test/parser/samples/script-comment-trailing-multiline/output.json b/test/parser/samples/script-comment-trailing-multiline/output.json
index a32ff799f7..d4a45911a1 100644
--- a/test/parser/samples/script-comment-trailing-multiline/output.json
+++ b/test/parser/samples/script-comment-trailing-multiline/output.json
@@ -44,46 +44,43 @@
 			}
 		]
 	},
-	"css": [],
-	"js": [
-		{
-			"start": 0,
-			"end": 77,
-			"attributes": [],
-			"content": {
-				"type": "Program",
-				"start": 8,
-				"end": 68,
-				"body": [
-					{
-						"type": "VariableDeclaration",
-						"start": 10,
-						"end": 29,
-						"declarations": [
-							{
-								"type": "VariableDeclarator",
+	"instance": {
+		"start": 0,
+		"end": 77,
+		"context": "default",
+		"content": {
+			"type": "Program",
+			"start": 8,
+			"end": 68,
+			"body": [
+				{
+					"type": "VariableDeclaration",
+					"start": 10,
+					"end": 29,
+					"declarations": [
+						{
+							"type": "VariableDeclarator",
+							"start": 14,
+							"end": 28,
+							"id": {
+								"type": "Identifier",
 								"start": 14,
+								"end": 18,
+								"name": "name"
+							},
+							"init": {
+								"type": "Literal",
+								"start": 21,
 								"end": 28,
-								"id": {
-									"type": "Identifier",
-									"start": 14,
-									"end": 18,
-									"name": "name"
-								},
-								"init": {
-									"type": "Literal",
-									"start": 21,
-									"end": 28,
-									"value": "world",
-									"raw": "'world'"
-								}
+								"value": "world",
+								"raw": "'world'"
 							}
-						],
-						"kind": "let"
-					}
-				],
-				"sourceType": "module"
-			}
+						}
+					],
+					"kind": "let"
+				}
+			],
+			"sourceType": "module"
 		}
-	]
+	}
 }
\ No newline at end of file
diff --git a/test/parser/samples/script-comment-trailing/output.json b/test/parser/samples/script-comment-trailing/output.json
index 78012d4438..92d431b558 100644
--- a/test/parser/samples/script-comment-trailing/output.json
+++ b/test/parser/samples/script-comment-trailing/output.json
@@ -44,46 +44,43 @@
 			}
 		]
 	},
-	"css": [],
-	"js": [
-		{
-			"start": 0,
-			"end": 66,
-			"attributes": [],
-			"content": {
-				"type": "Program",
-				"start": 8,
-				"end": 57,
-				"body": [
-					{
-						"type": "VariableDeclaration",
-						"start": 10,
-						"end": 29,
-						"declarations": [
-							{
-								"type": "VariableDeclarator",
+	"instance": {
+		"start": 0,
+		"end": 66,
+		"context": "default",
+		"content": {
+			"type": "Program",
+			"start": 8,
+			"end": 57,
+			"body": [
+				{
+					"type": "VariableDeclaration",
+					"start": 10,
+					"end": 29,
+					"declarations": [
+						{
+							"type": "VariableDeclarator",
+							"start": 14,
+							"end": 28,
+							"id": {
+								"type": "Identifier",
 								"start": 14,
+								"end": 18,
+								"name": "name"
+							},
+							"init": {
+								"type": "Literal",
+								"start": 21,
 								"end": 28,
-								"id": {
-									"type": "Identifier",
-									"start": 14,
-									"end": 18,
-									"name": "name"
-								},
-								"init": {
-									"type": "Literal",
-									"start": 21,
-									"end": 28,
-									"value": "world",
-									"raw": "'world'"
-								}
+								"value": "world",
+								"raw": "'world'"
 							}
-						],
-						"kind": "let"
-					}
-				],
-				"sourceType": "module"
-			}
+						}
+					],
+					"kind": "let"
+				}
+			],
+			"sourceType": "module"
 		}
-	]
+	}
 }
\ No newline at end of file
diff --git a/test/parser/samples/script/output.json b/test/parser/samples/script/output.json
index bf89a0ff02..95966f5dc6 100644
--- a/test/parser/samples/script/output.json
+++ b/test/parser/samples/script/output.json
@@ -44,46 +44,43 @@
 			}
 		]
 	},
-	"css": [],
-	"js": [
-		{
-			"start": 0,
-			"end": 39,
-			"attributes": [],
-			"content": {
-				"type": "Program",
-				"start": 8,
-				"end": 30,
-				"body": [
-					{
-						"type": "VariableDeclaration",
-						"start": 10,
-						"end": 29,
-						"declarations": [
-							{
-								"type": "VariableDeclarator",
+	"instance": {
+		"start": 0,
+		"end": 39,
+		"context": "default",
+		"content": {
+			"type": "Program",
+			"start": 8,
+			"end": 30,
+			"body": [
+				{
+					"type": "VariableDeclaration",
+					"start": 10,
+					"end": 29,
+					"declarations": [
+						{
+							"type": "VariableDeclarator",
+							"start": 14,
+							"end": 28,
+							"id": {
+								"type": "Identifier",
 								"start": 14,
+								"end": 18,
+								"name": "name"
+							},
+							"init": {
+								"type": "Literal",
+								"start": 21,
 								"end": 28,
-								"id": {
-									"type": "Identifier",
-									"start": 14,
-									"end": 18,
-									"name": "name"
-								},
-								"init": {
-									"type": "Literal",
-									"start": 21,
-									"end": 28,
-									"value": "world",
-									"raw": "'world'"
-								}
+								"value": "world",
+								"raw": "'world'"
 							}
-						],
-						"kind": "let"
-					}
-				],
-				"sourceType": "module"
-			}
+						}
+					],
+					"kind": "let"
+				}
+			],
+			"sourceType": "module"
 		}
-	]
+	}
 }
\ No newline at end of file
diff --git a/test/parser/samples/self-closing-element/output.json b/test/parser/samples/self-closing-element/output.json
index f7475b41d7..47eea2f333 100644
--- a/test/parser/samples/self-closing-element/output.json
+++ b/test/parser/samples/self-closing-element/output.json
@@ -14,6 +14,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/self-reference/output.json b/test/parser/samples/self-reference/output.json
index ff111e17ba..de5112a087 100644
--- a/test/parser/samples/self-reference/output.json
+++ b/test/parser/samples/self-reference/output.json
@@ -73,6 +73,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/space-between-mustaches/output.json b/test/parser/samples/space-between-mustaches/output.json
index d66735213d..c89409daeb 100644
--- a/test/parser/samples/space-between-mustaches/output.json
+++ b/test/parser/samples/space-between-mustaches/output.json
@@ -72,6 +72,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/spread/output.json b/test/parser/samples/spread/output.json
index a632dc89ab..3986da3578 100644
--- a/test/parser/samples/spread/output.json
+++ b/test/parser/samples/spread/output.json
@@ -26,6 +26,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/textarea-children/output.json b/test/parser/samples/textarea-children/output.json
index 08f42919ed..3b403458fc 100644
--- a/test/parser/samples/textarea-children/output.json
+++ b/test/parser/samples/textarea-children/output.json
@@ -44,6 +44,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/transition-intro-no-params/output.json b/test/parser/samples/transition-intro-no-params/output.json
index d53b4d9d88..edb15457e6 100644
--- a/test/parser/samples/transition-intro-no-params/output.json
+++ b/test/parser/samples/transition-intro-no-params/output.json
@@ -32,6 +32,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/transition-intro/output.json b/test/parser/samples/transition-intro/output.json
index 6405d0bad5..5583dd89bc 100644
--- a/test/parser/samples/transition-intro/output.json
+++ b/test/parser/samples/transition-intro/output.json
@@ -60,6 +60,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/unusual-identifier/output.json b/test/parser/samples/unusual-identifier/output.json
index fe9b175128..c0d4ecc3ff 100644
--- a/test/parser/samples/unusual-identifier/output.json
+++ b/test/parser/samples/unusual-identifier/output.json
@@ -45,6 +45,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/whitespace-leading-trailing/output.json b/test/parser/samples/whitespace-leading-trailing/output.json
index 7dbf259721..f164af01ba 100644
--- a/test/parser/samples/whitespace-leading-trailing/output.json
+++ b/test/parser/samples/whitespace-leading-trailing/output.json
@@ -27,6 +27,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/whitespace-normal/output.json b/test/parser/samples/whitespace-normal/output.json
index c150b53f04..e4ce956331 100644
--- a/test/parser/samples/whitespace-normal/output.json
+++ b/test/parser/samples/whitespace-normal/output.json
@@ -62,6 +62,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/parser/samples/yield/output.json b/test/parser/samples/yield/output.json
index b16ab436f6..16ea79d8e8 100644
--- a/test/parser/samples/yield/output.json
+++ b/test/parser/samples/yield/output.json
@@ -17,6 +17,7 @@
 			}
 		]
 	},
-	"css": [],
-	"js": []
+	"css": null,
+	"instance": null,
+	"module": null
 }
\ No newline at end of file
diff --git a/test/stats/samples/implicit-action/_config.js b/test/stats/samples/implicit-action/_config.js
new file mode 100644
index 0000000000..71c255d54b
--- /dev/null
+++ b/test/stats/samples/implicit-action/_config.js
@@ -0,0 +1,5 @@
+export default {
+	test(assert, stats) {
+		assert.deepEqual(stats.vars, []);
+	},
+};
diff --git a/test/stats/samples/implicit-action/input.html b/test/stats/samples/implicit-action/input.html
new file mode 100644
index 0000000000..466495d255
--- /dev/null
+++ b/test/stats/samples/implicit-action/input.html
@@ -0,0 +1 @@
+<div use:foo/>
\ No newline at end of file
diff --git a/test/stats/samples/implicit/_config.js b/test/stats/samples/implicit/_config.js
new file mode 100644
index 0000000000..71c255d54b
--- /dev/null
+++ b/test/stats/samples/implicit/_config.js
@@ -0,0 +1,5 @@
+export default {
+	test(assert, stats) {
+		assert.deepEqual(stats.vars, []);
+	},
+};
diff --git a/test/stats/samples/implicit/input.html b/test/stats/samples/implicit/input.html
new file mode 100644
index 0000000000..e076ac55f8
--- /dev/null
+++ b/test/stats/samples/implicit/input.html
@@ -0,0 +1 @@
+{foo}
\ No newline at end of file
diff --git a/test/stats/samples/imports/_config.js b/test/stats/samples/imports/_config.js
index f7c15183dd..287c5837ad 100644
--- a/test/stats/samples/imports/_config.js
+++ b/test/stats/samples/imports/_config.js
@@ -1,17 +1,35 @@
 export default {
 	test(assert, stats) {
-		assert.deepEqual(stats.imports, [
+		assert.deepEqual(stats.vars, [
 			{
-				source: 'x',
-				specifiers: [{ name: 'default', as: 'x' }]
+				name: 'x',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: false,
+				writable: false
 			},
 			{
-				source: 'y',
-				specifiers: [{ name: 'y', as: 'y' }]
+				name: 'y',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: false,
+				writable: false
 			},
 			{
-				source: 'z',
-				specifiers: [{ name: '*', as: 'z' }]
+				name: 'z',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: false,
+				writable: false
 			}
 		]);
 	}
diff --git a/test/stats/samples/mutated-vs-reassigned-bindings/_config.js b/test/stats/samples/mutated-vs-reassigned-bindings/_config.js
new file mode 100644
index 0000000000..a1d027cbb5
--- /dev/null
+++ b/test/stats/samples/mutated-vs-reassigned-bindings/_config.js
@@ -0,0 +1,26 @@
+export default {
+	test(assert, stats) {
+		assert.deepEqual(stats.vars, [
+			{
+				name: 'count',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: true,
+				referenced: true,
+				writable: true
+			},
+			{
+				name: 'user',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: true,
+				reassigned: false,
+				referenced: true,
+				writable: false
+			}
+		]);
+	}
+};
\ No newline at end of file
diff --git a/test/stats/samples/mutated-vs-reassigned-bindings/input.html b/test/stats/samples/mutated-vs-reassigned-bindings/input.html
new file mode 100644
index 0000000000..c1186ad3a7
--- /dev/null
+++ b/test/stats/samples/mutated-vs-reassigned-bindings/input.html
@@ -0,0 +1,7 @@
+<script>
+	let count;
+	const user = { name: 'world' };
+</script>
+
+<input bind:value={user.name}>
+<input type=number bind:value={count}>
\ No newline at end of file
diff --git a/test/stats/samples/mutated-vs-reassigned/_config.js b/test/stats/samples/mutated-vs-reassigned/_config.js
new file mode 100644
index 0000000000..a1d027cbb5
--- /dev/null
+++ b/test/stats/samples/mutated-vs-reassigned/_config.js
@@ -0,0 +1,26 @@
+export default {
+	test(assert, stats) {
+		assert.deepEqual(stats.vars, [
+			{
+				name: 'count',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: true,
+				referenced: true,
+				writable: true
+			},
+			{
+				name: 'user',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: true,
+				reassigned: false,
+				referenced: true,
+				writable: false
+			}
+		]);
+	}
+};
\ No newline at end of file
diff --git a/test/stats/samples/mutated-vs-reassigned/input.html b/test/stats/samples/mutated-vs-reassigned/input.html
new file mode 100644
index 0000000000..6a5e604c26
--- /dev/null
+++ b/test/stats/samples/mutated-vs-reassigned/input.html
@@ -0,0 +1,7 @@
+<script>
+	let count;
+	const user = { name: 'world' };
+</script>
+
+<input on:input="{e => user.name = e.value}">
+<input type=number on:input="{e => count = +e.value}">
\ No newline at end of file
diff --git a/test/stats/samples/props/_config.js b/test/stats/samples/props/_config.js
index 50c01e2ffb..cbec831f61 100644
--- a/test/stats/samples/props/_config.js
+++ b/test/stats/samples/props/_config.js
@@ -1,5 +1,46 @@
 export default {
 	test(assert, stats) {
-		assert.deepEqual(stats.props.sort(), ['cats', 'name']);
+		assert.deepEqual(stats.vars, [
+			{
+				name: 'name',
+				export_name: 'name',
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: true,
+				writable: true
+			},
+			{
+				name: 'cats',
+				export_name: 'cats',
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: true,
+				writable: true
+			},
+			{
+				name: 'foo',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: false,
+				writable: true
+			},
+			{
+				name: 'bar',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: true,
+				referenced: true,
+				writable: true
+			}
+		]);
 	}
 };
\ No newline at end of file
diff --git a/test/stats/samples/store-referenced/_config.js b/test/stats/samples/store-referenced/_config.js
new file mode 100644
index 0000000000..ecb958df1e
--- /dev/null
+++ b/test/stats/samples/store-referenced/_config.js
@@ -0,0 +1,26 @@
+export default {
+	test(assert, stats) {
+		assert.deepEqual(stats.vars, [
+			{
+				name: 'foo',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: true,
+				writable: true
+			},
+			{
+				name: '$foo',
+				export_name: null,
+				injected: true,
+				module: false,
+				mutated: true,
+				reassigned: false,
+				referenced: true,
+				writable: true
+			}
+		]);
+	}
+};
diff --git a/test/stats/samples/store-referenced/input.html b/test/stats/samples/store-referenced/input.html
new file mode 100644
index 0000000000..0222372f4d
--- /dev/null
+++ b/test/stats/samples/store-referenced/input.html
@@ -0,0 +1,5 @@
+<script>
+	let foo;
+</script>
+
+{$foo}
\ No newline at end of file
diff --git a/test/stats/samples/store-unreferenced/_config.js b/test/stats/samples/store-unreferenced/_config.js
new file mode 100644
index 0000000000..9ea5784f92
--- /dev/null
+++ b/test/stats/samples/store-unreferenced/_config.js
@@ -0,0 +1,26 @@
+export default {
+	test(assert, stats) {
+		assert.deepEqual(stats.vars, [
+			{
+				name: 'foo',
+				export_name: null,
+				injected: false,
+				module: false,
+				mutated: false,
+				reassigned: false,
+				referenced: true,
+				writable: true
+			},
+			{
+				name: '$foo',
+				export_name: null,
+				injected: true,
+				module: false,
+				mutated: true,
+				reassigned: false,
+				referenced: false,
+				writable: true
+			}
+		]);
+	}
+};
diff --git a/test/stats/samples/store-unreferenced/input.html b/test/stats/samples/store-unreferenced/input.html
new file mode 100644
index 0000000000..3ad525047d
--- /dev/null
+++ b/test/stats/samples/store-unreferenced/input.html
@@ -0,0 +1,4 @@
+<script>
+	let foo;
+	console.log($foo);
+</script>
\ No newline at end of file
diff --git a/test/stats/samples/template-references/_config.js b/test/stats/samples/template-references/_config.js
index 0bc844928f..71c255d54b 100644
--- a/test/stats/samples/template-references/_config.js
+++ b/test/stats/samples/template-references/_config.js
@@ -1,8 +1,5 @@
 export default {
 	test(assert, stats) {
-		assert.equal(stats.templateReferences.size, 3);
-		assert.ok(stats.templateReferences.has('foo'));
-		assert.ok(stats.templateReferences.has('Bar'));
-		assert.ok(stats.templateReferences.has('baz'));
+		assert.deepEqual(stats.vars, []);
 	},
 };
diff --git a/test/stats/samples/undeclared/_config.js b/test/stats/samples/undeclared/_config.js
new file mode 100644
index 0000000000..71c255d54b
--- /dev/null
+++ b/test/stats/samples/undeclared/_config.js
@@ -0,0 +1,5 @@
+export default {
+	test(assert, stats) {
+		assert.deepEqual(stats.vars, []);
+	},
+};
diff --git a/test/stats/samples/undeclared/input.html b/test/stats/samples/undeclared/input.html
new file mode 100644
index 0000000000..0178466fe3
--- /dev/null
+++ b/test/stats/samples/undeclared/input.html
@@ -0,0 +1,3 @@
+<script></script>
+
+{foo}
\ No newline at end of file
diff --git a/test/validator/index.js b/test/validator/index.js
index 9ca3fc57d1..517427e817 100644
--- a/test/validator/index.js
+++ b/test/validator/index.js
@@ -64,11 +64,16 @@ describe("validate", () => {
 					throw new Error(`Expected an error: ${expected.message}`);
 				}
 
-				assert.equal(error.code, expected.code);
-				assert.equal(error.message, expected.message);
-				assert.deepEqual(error.start, expected.start);
-				assert.deepEqual(error.end, expected.end);
-				assert.equal(error.pos, expected.pos);
+				try {
+					assert.equal(error.code, expected.code);
+					assert.equal(error.message, expected.message);
+					assert.deepEqual(error.start, expected.start);
+					assert.deepEqual(error.end, expected.end);
+					assert.equal(error.pos, expected.pos);
+				} catch (e) {
+					console.error(error)
+					throw e;
+				}
 			}
 		});
 	});
@@ -102,7 +107,7 @@ describe("validate", () => {
 			name: "_",
 			generate: false
 		});
-		
+
 		assert.deepEqual(stats.warnings, []);
 	});
 });
diff --git a/test/validator/samples/binding-invalid-value/errors.json b/test/validator/samples/binding-invalid-value/errors.json
new file mode 100644
index 0000000000..07eb7b0f1c
--- /dev/null
+++ b/test/validator/samples/binding-invalid-value/errors.json
@@ -0,0 +1,15 @@
+[{
+	"code": "binding-undeclared",
+	"message": "foo is not declared",
+	"pos": 37,
+	"start": {
+		"line": 2,
+		"column": 19,
+		"character": 37
+	},
+	"end": {
+		"line": 2,
+		"column": 22,
+		"character": 40
+	}
+}]
\ No newline at end of file
diff --git a/test/validator/samples/binding-invalid-value/input.html b/test/validator/samples/binding-invalid-value/input.html
new file mode 100644
index 0000000000..9eeeba5fed
--- /dev/null
+++ b/test/validator/samples/binding-invalid-value/input.html
@@ -0,0 +1,2 @@
+<script></script>
+<input bind:value={foo}>
\ No newline at end of file
diff --git a/test/validator/samples/multiple-script-default-context/errors.json b/test/validator/samples/multiple-script-default-context/errors.json
index 6e9a53d691..6f0637b51b 100644
--- a/test/validator/samples/multiple-script-default-context/errors.json
+++ b/test/validator/samples/multiple-script-default-context/errors.json
@@ -8,8 +8,8 @@
 		"character": 30
 	},
 	"end": {
-		"line": 7,
-		"column": 9,
-		"character": 58
+		"line": 5,
+		"column": 0,
+		"character": 30
 	}
 }]
\ No newline at end of file
diff --git a/test/validator/samples/multiple-script-module-context/errors.json b/test/validator/samples/multiple-script-module-context/errors.json
index ef8dc15273..7b86c61926 100644
--- a/test/validator/samples/multiple-script-module-context/errors.json
+++ b/test/validator/samples/multiple-script-module-context/errors.json
@@ -8,8 +8,8 @@
 		"character": 47
 	},
 	"end": {
-		"line": 7,
-		"column": 9,
-		"character": 92
+		"line": 5,
+		"column": 0,
+		"character": 47
 	}
 }]
\ No newline at end of file
diff --git a/test/validator/samples/script-invalid-context/errors.json b/test/validator/samples/script-invalid-context/errors.json
index 9c276d932d..07ad0f1270 100644
--- a/test/validator/samples/script-invalid-context/errors.json
+++ b/test/validator/samples/script-invalid-context/errors.json
@@ -9,7 +9,7 @@
 	},
 	"end": {
 		"line": 1,
-		"column": 22,
-		"character": 22
+		"column": 8,
+		"character": 8
 	}
 }]
\ No newline at end of file