From 9ff1beec489033c40d76e52d56669a25fcbdeb85 Mon Sep 17 00:00:00 2001
From: Rich Harris <richard.a.harris@gmail.com>
Date: Sun, 22 Apr 2018 21:09:04 -0400
Subject: [PATCH] WIP

---
 src/generators/Generator.ts                   | 618 +++++++++---------
 src/generators/dom/Block.ts                   |  18 +-
 src/generators/nodes/Attribute.ts             |  66 +-
 src/generators/nodes/Binding.ts               |  52 +-
 src/generators/nodes/Component.ts             |  45 +-
 src/generators/nodes/EachBlock.ts             |  61 +-
 src/generators/nodes/Element.ts               | 308 +++++----
 src/generators/nodes/ElseBlock.ts             |   7 +-
 src/generators/nodes/EventHandler.ts          |  58 +-
 src/generators/nodes/Fragment.ts              |   6 +-
 src/generators/nodes/IfBlock.ts               |  83 ++-
 src/generators/nodes/Text.ts                  |   5 +
 src/generators/nodes/Transition.ts            |  15 +-
 src/generators/nodes/Window.ts                | 151 +++--
 src/generators/nodes/shared/Expression.ts     |  63 +-
 src/generators/nodes/shared/Node.ts           |  13 +-
 src/generators/nodes/shared/Tag.ts            |  11 +-
 src/generators/nodes/shared/mapChildren.ts    |   8 +
 src/generators/server-side-rendering/Block.ts |   6 +-
 src/generators/server-side-rendering/index.ts |  10 +-
 .../visitors/AwaitBlock.ts                    |   3 +-
 .../visitors/Component.ts                     |   3 +-
 .../server-side-rendering/visitors/IfBlock.ts |   3 +-
 .../visitors/MustacheTag.ts                   |   3 +-
 src/parse/state/tag.ts                        |   2 +-
 src/utils/addToSet.ts                         |   5 +
 src/utils/annotateWithScopes.ts               |  48 ++
 src/utils/createDebuggingComment.ts           |  25 +-
 test/runtime/index.js                         |   2 +-
 .../samples/attribute-dynamic/_config.js      |   1 -
 .../samples/binding-input-text/_config.js     |  33 +-
 test/runtime/samples/trait-function/main.html |  46 --
 32 files changed, 998 insertions(+), 780 deletions(-)
 create mode 100644 src/utils/addToSet.ts
 delete mode 100644 test/runtime/samples/trait-function/main.html

diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts
index 0aa96ca9d2..c91c36caf2 100644
--- a/src/generators/Generator.ts
+++ b/src/generators/Generator.ts
@@ -194,7 +194,7 @@ export default class Generator {
 		}
 
 		this.fragment = new Fragment(this, parsed.html);
-		this.walkTemplate();
+		// this.walkTemplate();
 		if (!this.customElement) this.stylesheet.reify();
 	}
 
@@ -215,107 +215,107 @@ export default class Generator {
 		return this.aliases.get(name);
 	}
 
-	contextualise(
-		contexts: Map<string, string>,
-		indexes: Map<string, string>,
-		expression: Node,
-		context: string,
-		isEventHandler: boolean
-	): {
-		contexts: Set<string>,
-		indexes: Set<string>
-	} {
-		// this.addSourcemapLocations(expression);
-
-		const usedContexts: Set<string> = new Set();
-		const usedIndexes: Set<string> = new Set();
-
-		const { code, helpers } = this;
-
-		let scope: Scope;
-		let lexicalDepth = 0;
-
-		const self = this;
-
-		walk(expression, {
-			enter(node: Node, parent: Node, key: string) {
-				if (/^Function/.test(node.type)) lexicalDepth += 1;
-
-				if (node._scope) {
-					scope = node._scope;
-					return;
-				}
-
-				if (node.type === 'ThisExpression') {
-					if (lexicalDepth === 0 && context)
-						code.overwrite(node.start, node.end, context, {
-							storeName: true,
-							contentOnly: false,
-						});
-				} else if (isReference(node, parent)) {
-					const { name } = flattenReference(node);
-					if (scope && scope.has(name)) return;
-
-					if (name === 'event' && isEventHandler) {
-						// noop
-					} else if (contexts.has(name)) {
-						const contextName = contexts.get(name);
-						if (contextName !== name) {
-							// this is true for 'reserved' names like `state` and `component`,
-							// also destructured contexts
-							code.overwrite(
-								node.start,
-								node.start + name.length,
-								contextName,
-								{ storeName: true, contentOnly: false }
-							);
-
-							const destructuredName = contextName.replace(/\[\d+\]/, '');
-							if (destructuredName !== contextName) {
-								// so that hoisting the context works correctly
-								usedContexts.add(destructuredName);
-							}
-						}
-
-						usedContexts.add(name);
-					} else if (helpers.has(name)) {
-						let object = node;
-						while (object.type === 'MemberExpression') object = object.object;
-
-						const alias = self.templateVars.get(`helpers-${name}`);
-						if (alias !== name) code.overwrite(object.start, object.end, alias);
-					} else if (indexes.has(name)) {
-						const context = indexes.get(name);
-						usedContexts.add(context); // TODO is this right?
-						usedIndexes.add(name);
-					} else {
-						// handle shorthand properties
-						if (parent && parent.type === 'Property' && parent.shorthand) {
-							if (key === 'key') {
-								code.appendLeft(node.start, `${name}: `);
-								return;
-							}
-						}
-
-						code.prependRight(node.start, `state.`);
-						usedContexts.add('state');
-					}
-
-					this.skip();
-				}
-			},
-
-			leave(node: Node) {
-				if (/^Function/.test(node.type)) lexicalDepth -= 1;
-				if (node._scope) scope = scope.parent;
-			},
-		});
-
-		return {
-			contexts: usedContexts,
-			indexes: usedIndexes
-		};
-	}
+	// contextualise(
+	// 	contexts: Map<string, string>,
+	// 	indexes: Map<string, string>,
+	// 	expression: Node,
+	// 	context: string,
+	// 	isEventHandler: boolean
+	// ): {
+	// 	contexts: Set<string>,
+	// 	indexes: Set<string>
+	// } {
+	// 	// this.addSourcemapLocations(expression);
+
+	// 	const usedContexts: Set<string> = new Set();
+	// 	const usedIndexes: Set<string> = new Set();
+
+	// 	const { code, helpers } = this;
+
+	// 	let scope: Scope;
+	// 	let lexicalDepth = 0;
+
+	// 	const self = this;
+
+	// 	walk(expression, {
+	// 		enter(node: Node, parent: Node, key: string) {
+	// 			if (/^Function/.test(node.type)) lexicalDepth += 1;
+
+	// 			if (node._scope) {
+	// 				scope = node._scope;
+	// 				return;
+	// 			}
+
+	// 			if (node.type === 'ThisExpression') {
+	// 				if (lexicalDepth === 0 && context)
+	// 					code.overwrite(node.start, node.end, context, {
+	// 						storeName: true,
+	// 						contentOnly: false,
+	// 					});
+	// 			} else if (isReference(node, parent)) {
+	// 				const { name } = flattenReference(node);
+	// 				if (scope && scope.has(name)) return;
+
+	// 				if (name === 'event' && isEventHandler) {
+	// 					// noop
+	// 				} else if (contexts.has(name)) {
+	// 					const contextName = contexts.get(name);
+	// 					if (contextName !== name) {
+	// 						// this is true for 'reserved' names like `state` and `component`,
+	// 						// also destructured contexts
+	// 						code.overwrite(
+	// 							node.start,
+	// 							node.start + name.length,
+	// 							contextName,
+	// 							{ storeName: true, contentOnly: false }
+	// 						);
+
+	// 						const destructuredName = contextName.replace(/\[\d+\]/, '');
+	// 						if (destructuredName !== contextName) {
+	// 							// so that hoisting the context works correctly
+	// 							usedContexts.add(destructuredName);
+	// 						}
+	// 					}
+
+	// 					usedContexts.add(name);
+	// 				} else if (helpers.has(name)) {
+	// 					let object = node;
+	// 					while (object.type === 'MemberExpression') object = object.object;
+
+	// 					const alias = self.templateVars.get(`helpers-${name}`);
+	// 					if (alias !== name) code.overwrite(object.start, object.end, alias);
+	// 				} else if (indexes.has(name)) {
+	// 					const context = indexes.get(name);
+	// 					usedContexts.add(context); // TODO is this right?
+	// 					usedIndexes.add(name);
+	// 				} else {
+	// 					// handle shorthand properties
+	// 					if (parent && parent.type === 'Property' && parent.shorthand) {
+	// 						if (key === 'key') {
+	// 							code.appendLeft(node.start, `${name}: `);
+	// 							return;
+	// 						}
+	// 					}
+
+	// 					code.prependRight(node.start, `state.`);
+	// 					usedContexts.add('state');
+	// 				}
+
+	// 				this.skip();
+	// 			}
+	// 		},
+
+	// 		leave(node: Node) {
+	// 			if (/^Function/.test(node.type)) lexicalDepth -= 1;
+	// 			if (node._scope) scope = scope.parent;
+	// 		},
+	// 	});
+
+	// 	return {
+	// 		contexts: usedContexts,
+	// 		indexes: usedIndexes
+	// 	};
+	// }
 
 	generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) {
 		const pattern = /\[✂(\d+)-(\d+)$/;
@@ -707,211 +707,211 @@ export default class Generator {
 		}
 	}
 
-	walkTemplate() {
-		const generator = this;
-		const {
-			code,
-			expectedProperties,
-			helpers
-		} = this;
-
-		const contextualise = (
-			node: Node, contextDependencies: Map<string, string[]>,
-			indexes: Set<string>,
-			isEventHandler: boolean
-		) => {
-			this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else?
-			let { scope } = annotateWithScopes(node);
-
-			const dependencies: Set<string> = new Set();
-
-			walk(node, {
-				enter(node: Node, parent: Node) {
-					code.addSourcemapLocation(node.start);
-					code.addSourcemapLocation(node.end);
-
-					if (node._scope) {
-						scope = node._scope;
-						return;
-					}
-
-					if (isReference(node, parent)) {
-						const { name } = flattenReference(node);
-						if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
-
-						if (contextDependencies.has(name)) {
-							contextDependencies.get(name).forEach(dependency => {
-								dependencies.add(dependency);
-							});
-						} else if (!indexes.has(name)) {
-							dependencies.add(name);
-						}
-
-						this.skip();
-					}
-				},
-
-				leave(node: Node, parent: Node) {
-					if (node._scope) scope = scope.parent;
-				}
-			});
-
-			dependencies.forEach(dependency => {
-				expectedProperties.add(dependency);
-			});
-
-			return {
-				snippet: `[✂${node.start}-${node.end}✂]`,
-				dependencies: Array.from(dependencies)
-			};
-		}
-
-		const contextStack = [];
-		const indexStack = [];
-		const dependenciesStack = [];
-
-		let contextDependencies = new Map();
-		const contextDependenciesStack: Map<string, string[]>[] = [contextDependencies];
-
-		let indexes = new Set();
-		const indexesStack: Set<string>[] = [indexes];
-
-		function parentIsHead(node) {
-			if (!node) return false;
-			if (node.type === 'Component' || node.type === 'Element') return false;
-			if (node.type === 'Head') return true;
-
-			return parentIsHead(node.parent);
-		}
-
-		walk(this.fragment, {
-			enter(node: Node, parent: Node, key: string) {
-				// TODO this is hacky as hell
-				if (key === 'parent') return this.skip();
-				node.parent = parent;
-
-				node.generator = generator;
-
-				if (node.type === 'Element' && (node.name === 'svelte:component' || node.name === 'svelte:self' || generator.components.has(node.name))) {
-					node.type = 'Component';
-					Object.setPrototypeOf(node, nodes.Component.prototype);
-				} else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse?
-					node.type = 'Title';
-					Object.setPrototypeOf(node, nodes.Title.prototype);
-				} else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) {
-					node.type = 'Slot';
-					Object.setPrototypeOf(node, nodes.Slot.prototype);
-				} else if (node.type in nodes) {
-					Object.setPrototypeOf(node, nodes[node.type].prototype);
-				}
-
-				if (node.type === 'Element') {
-					generator.stylesheet.apply(node);
-				}
-
-				if (node.type === 'EachBlock') {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-
-					contextDependencies = new Map(contextDependencies);
-					contextDependencies.set(node.context, node.metadata.dependencies);
-
-					if (node.destructuredContexts) {
-						node.destructuredContexts.forEach((name: string) => {
-							contextDependencies.set(name, node.metadata.dependencies);
-						});
-					}
-
-					contextDependenciesStack.push(contextDependencies);
-
-					if (node.index) {
-						indexes = new Set(indexes);
-						indexes.add(node.index);
-						indexesStack.push(indexes);
-					}
-				}
-
-				if (node.type === 'AwaitBlock') {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-
-					contextDependencies = new Map(contextDependencies);
-					contextDependencies.set(node.value, node.metadata.dependencies);
-					contextDependencies.set(node.error, node.metadata.dependencies);
-
-					contextDependenciesStack.push(contextDependencies);
-				}
-
-				if (node.type === 'IfBlock') {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-				}
-
-				if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-					this.skip();
-				}
-
-				if (node.type === 'Binding') {
-					node.metadata = contextualise(node.value, contextDependencies, indexes, false);
-					this.skip();
-				}
-
-				if (node.type === 'EventHandler' && node.expression) {
-					node.expression.arguments.forEach((arg: Node) => {
-						arg.metadata = contextualise(arg, contextDependencies, indexes, true);
-					});
-					this.skip();
-				}
-
-				if (node.type === 'Transition' && node.expression) {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-					this.skip();
-				}
-
-				if (node.type === 'Action' && node.expression) {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-					if (node.expression.type === 'CallExpression') {
-						node.expression.arguments.forEach((arg: Node) => {
-							arg.metadata = contextualise(arg, contextDependencies, indexes, true);
-						});
-					}
-					this.skip();
-				}
-
-				if (node.type === 'Component' && node.name === 'svelte:component') {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-				}
-
-				if (node.type === 'Spread') {
-					node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
-				}
-			},
-
-			leave(node: Node, parent: Node) {
-				if (node.type === 'EachBlock') {
-					contextDependenciesStack.pop();
-					contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1];
-
-					if (node.index) {
-						indexesStack.pop();
-						indexes = indexesStack[indexesStack.length - 1];
-					}
-				}
-
-				if (node.type === 'Element' && node.name === 'option') {
-					// Special case — treat these the same way:
-					//   <option>{{foo}}</option>
-					//   <option value='{{foo}}'>{{foo}}</option>
-					const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
-
-					if (!valueAttribute) {
-						node.attributes.push(new nodes.Attribute({
-							generator,
-							name: 'value',
-							value: node.children,
-							parent: node
-						}));
-					}
-				}
-			}
-		});
-	}
+	// walkTemplate() {
+	// 	const generator = this;
+	// 	const {
+	// 		code,
+	// 		expectedProperties,
+	// 		helpers
+	// 	} = this;
+
+	// 	const contextualise = (
+	// 		node: Node, contextDependencies: Map<string, string[]>,
+	// 		indexes: Set<string>,
+	// 		isEventHandler: boolean
+	// 	) => {
+	// 		this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else?
+	// 		let { scope } = annotateWithScopes(node);
+
+	// 		const dependencies: Set<string> = new Set();
+
+	// 		walk(node, {
+	// 			enter(node: Node, parent: Node) {
+	// 				code.addSourcemapLocation(node.start);
+	// 				code.addSourcemapLocation(node.end);
+
+	// 				if (node._scope) {
+	// 					scope = node._scope;
+	// 					return;
+	// 				}
+
+	// 				if (isReference(node, parent)) {
+	// 					const { name } = flattenReference(node);
+	// 					if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
+
+	// 					if (contextDependencies.has(name)) {
+	// 						contextDependencies.get(name).forEach(dependency => {
+	// 							dependencies.add(dependency);
+	// 						});
+	// 					} else if (!indexes.has(name)) {
+	// 						dependencies.add(name);
+	// 					}
+
+	// 					this.skip();
+	// 				}
+	// 			},
+
+	// 			leave(node: Node, parent: Node) {
+	// 				if (node._scope) scope = scope.parent;
+	// 			}
+	// 		});
+
+	// 		dependencies.forEach(dependency => {
+	// 			expectedProperties.add(dependency);
+	// 		});
+
+	// 		return {
+	// 			snippet: `[✂${node.start}-${node.end}✂]`,
+	// 			dependencies: Array.from(dependencies)
+	// 		};
+	// 	}
+
+	// 	const contextStack = [];
+	// 	const indexStack = [];
+	// 	const dependenciesStack = [];
+
+	// 	let contextDependencies = new Map();
+	// 	const contextDependenciesStack: Map<string, string[]>[] = [contextDependencies];
+
+	// 	let indexes = new Set();
+	// 	const indexesStack: Set<string>[] = [indexes];
+
+	// 	function parentIsHead(node) {
+	// 		if (!node) return false;
+	// 		if (node.type === 'Component' || node.type === 'Element') return false;
+	// 		if (node.type === 'Head') return true;
+
+	// 		return parentIsHead(node.parent);
+	// 	}
+
+	// 	walk(this.fragment, {
+	// 		enter(node: Node, parent: Node, key: string) {
+	// 			// TODO this is hacky as hell
+	// 			if (key === 'parent') return this.skip();
+	// 			node.parent = parent;
+
+	// 			node.generator = generator;
+
+	// 			if (node.type === 'Element' && (node.name === 'svelte:component' || node.name === 'svelte:self' || generator.components.has(node.name))) {
+	// 				node.type = 'Component';
+	// 				Object.setPrototypeOf(node, nodes.Component.prototype);
+	// 			} else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse?
+	// 				node.type = 'Title';
+	// 				Object.setPrototypeOf(node, nodes.Title.prototype);
+	// 			} else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) {
+	// 				node.type = 'Slot';
+	// 				Object.setPrototypeOf(node, nodes.Slot.prototype);
+	// 			} else if (node.type in nodes) {
+	// 				Object.setPrototypeOf(node, nodes[node.type].prototype);
+	// 			}
+
+	// 			if (node.type === 'Element') {
+	// 				generator.stylesheet.apply(node);
+	// 			}
+
+	// 			if (node.type === 'EachBlock') {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+
+	// 				contextDependencies = new Map(contextDependencies);
+	// 				contextDependencies.set(node.context, node.metadata.dependencies);
+
+	// 				if (node.destructuredContexts) {
+	// 					node.destructuredContexts.forEach((name: string) => {
+	// 						contextDependencies.set(name, node.metadata.dependencies);
+	// 					});
+	// 				}
+
+	// 				contextDependenciesStack.push(contextDependencies);
+
+	// 				if (node.index) {
+	// 					indexes = new Set(indexes);
+	// 					indexes.add(node.index);
+	// 					indexesStack.push(indexes);
+	// 				}
+	// 			}
+
+	// 			if (node.type === 'AwaitBlock') {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+
+	// 				contextDependencies = new Map(contextDependencies);
+	// 				contextDependencies.set(node.value, node.metadata.dependencies);
+	// 				contextDependencies.set(node.error, node.metadata.dependencies);
+
+	// 				contextDependenciesStack.push(contextDependencies);
+	// 			}
+
+	// 			if (node.type === 'IfBlock') {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+	// 			}
+
+	// 			if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+	// 				this.skip();
+	// 			}
+
+	// 			if (node.type === 'Binding') {
+	// 				node.metadata = contextualise(node.value, contextDependencies, indexes, false);
+	// 				this.skip();
+	// 			}
+
+	// 			if (node.type === 'EventHandler' && node.expression) {
+	// 				node.expression.arguments.forEach((arg: Node) => {
+	// 					arg.metadata = contextualise(arg, contextDependencies, indexes, true);
+	// 				});
+	// 				this.skip();
+	// 			}
+
+	// 			if (node.type === 'Transition' && node.expression) {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+	// 				this.skip();
+	// 			}
+
+	// 			if (node.type === 'Action' && node.expression) {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+	// 				if (node.expression.type === 'CallExpression') {
+	// 					node.expression.arguments.forEach((arg: Node) => {
+	// 						arg.metadata = contextualise(arg, contextDependencies, indexes, true);
+	// 					});
+	// 				}
+	// 				this.skip();
+	// 			}
+
+	// 			if (node.type === 'Component' && node.name === 'svelte:component') {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+	// 			}
+
+	// 			if (node.type === 'Spread') {
+	// 				node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
+	// 			}
+	// 		},
+
+	// 		leave(node: Node, parent: Node) {
+	// 			if (node.type === 'EachBlock') {
+	// 				contextDependenciesStack.pop();
+	// 				contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1];
+
+	// 				if (node.index) {
+	// 					indexesStack.pop();
+	// 					indexes = indexesStack[indexesStack.length - 1];
+	// 				}
+	// 			}
+
+	// 			if (node.type === 'Element' && node.name === 'option') {
+	// 				// Special case — treat these the same way:
+	// 				//   <option>{{foo}}</option>
+	// 				//   <option value='{{foo}}'>{{foo}}</option>
+	// 				const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
+
+	// 				if (!valueAttribute) {
+	// 					node.attributes.push(new nodes.Attribute({
+	// 						generator,
+	// 						name: 'value',
+	// 						value: node.children,
+	// 						parent: node
+	// 					}));
+	// 				}
+	// 			}
+	// 		}
+	// 	});
+	// }
 }
diff --git a/src/generators/dom/Block.ts b/src/generators/dom/Block.ts
index edf505c7b3..f554d49097 100644
--- a/src/generators/dom/Block.ts
+++ b/src/generators/dom/Block.ts
@@ -110,13 +110,13 @@ export default class Block {
 
 		this.aliases = new Map()
 			.set('component', this.getUniqueName('component'))
-			.set('state', this.getUniqueName('state'));
+			.set('ctx', this.getUniqueName('ctx'));
 		if (this.key) this.aliases.set('key', this.getUniqueName('key'));
 
 		this.hasUpdateMethod = false; // determined later
 	}
 
-	addDependencies(dependencies: string[]) {
+	addDependencies(dependencies: Set<string>) {
 		dependencies.forEach(dependency => {
 			this.dependencies.add(dependency);
 		});
@@ -163,10 +163,6 @@ export default class Block {
 		return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
 	}
 
-	contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
-		return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
-	}
-
 	toString() {
 		let introing;
 		const hasIntros = !this.builders.intro.isEmpty();
@@ -195,9 +191,9 @@ export default class Block {
 			const indexName = this.indexNames.get(context);
 
 			initializers.push(
-				`${name} = state.${context}`,
-				`${listName} = state.${listName}`,
-				`${indexName} = state.${indexName}`
+				`${name} = ctx.${context}`,
+				`${listName} = ctx.${listName}`,
+				`${indexName} = ctx.${indexName}`
 			);
 
 			this.hasUpdateMethod = true;
@@ -266,7 +262,7 @@ export default class Block {
 				properties.addBlock(`p: @noop,`);
 			} else {
 				properties.addBlock(deindent`
-					p: function update(changed, state) {
+					p: function update(changed, ctx) {
 						${initializers.map(str => `${str};`)}
 						${this.builders.update}
 					},
@@ -338,7 +334,7 @@ export default class Block {
 
 		return deindent`
 			${this.comment && `// ${escape(this.comment)}`}
-			function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, state) {
+			function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) {
 				${initializers.length > 0 &&
 					`var ${initializers.join(', ')};`}
 				${this.variables.size > 0 &&
diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts
index 074cc18352..e44aad898c 100644
--- a/src/generators/nodes/Attribute.ts
+++ b/src/generators/nodes/Attribute.ts
@@ -2,10 +2,12 @@ import deindent from '../../utils/deindent';
 import { stringify } from '../../utils/stringify';
 import fixAttributeCasing from '../../utils/fixAttributeCasing';
 import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
+import addToSet from '../../utils/addToSet';
 import { DomGenerator } from '../dom/index';
 import Node from './shared/Node';
 import Element from './Element';
 import Block from '../dom/Block';
+import Expression from './shared/Expression';
 
 export interface StyleProp {
 	key: string;
@@ -20,14 +22,32 @@ export default class Attribute extends Node {
 	compiler: DomGenerator;
 	parent: Element;
 	name: string;
-	value: true | Node[]
+	isTrue: boolean;
+	isDynamic: boolean;
+	chunks: Node[];
+	dependencies: Set<string>;
 	expression: Node;
 
 	constructor(compiler, parent, info) {
 		super(compiler, parent, info);
 
 		this.name = info.name;
-		this.value = info.value;
+		this.isTrue = info.value === true;
+
+		this.dependencies = new Set();
+
+		this.chunks = this.isTrue
+			? []
+			: info.value.map(node => {
+				if (node.type === 'Text') return node;
+
+				const expression = new Expression(compiler, this, node.expression);
+
+				addToSet(this.dependencies, expression.dependencies);
+				return expression;
+			});
+
+		this.isDynamic = this.dependencies.size > 0;
 	}
 
 	render(block: Block) {
@@ -35,7 +55,7 @@ export default class Attribute extends Node {
 		const name = fixAttributeCasing(this.name);
 
 		if (name === 'style') {
-			const styleProps = optimizeStyle(this.value);
+			const styleProps = optimizeStyle(this.chunks);
 			if (styleProps) {
 				this.renderStyle(block, styleProps);
 				return;
@@ -66,15 +86,14 @@ export default class Attribute extends Node {
 			? '@setXlinkAttribute'
 			: '@setAttribute';
 
-		const isDynamic = this.isDynamic();
-		const isLegacyInputType = this.generator.legacy && name === 'type' && this.parent.name === 'input';
+		const isLegacyInputType = this.compiler.legacy && name === 'type' && this.parent.name === 'input';
 
-		const isDataSet = /^data-/.test(name) && !this.generator.legacy && !node.namespace;
+		const isDataSet = /^data-/.test(name) && !this.compiler.legacy && !node.namespace;
 		const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
 			return m[1].toUpperCase();
 		}) : name;
 
-		if (isDynamic) {
+		if (this.isDynamic) {
 			let value;
 
 			const allDependencies = new Set();
@@ -83,11 +102,10 @@ export default class Attribute extends Node {
 
 			// TODO some of this code is repeated in Tag.ts — would be good to
 			// DRY it out if that's possible without introducing crazy indirection
-			if (this.value.length === 1) {
-				// single {{tag}} — may be a non-string
-				const { expression } = this.value[0];
-				const { indexes } = block.contextualise(expression);
-				const { dependencies, snippet } = this.value[0].metadata;
+			if (this.chunks.length === 1) {
+				// single {tag} — may be a non-string
+				const expression = this.chunks[0];
+				const { dependencies, snippet, indexes } = expression;
 
 				value = snippet;
 				dependencies.forEach(d => {
@@ -104,14 +122,13 @@ export default class Attribute extends Node {
 			} else {
 				// '{{foo}} {{bar}}' — treat as string concatenation
 				value =
-					(this.value[0].type === 'Text' ? '' : `"" + `) +
-					this.value
+					(this.chunks[0].type === 'Text' ? '' : `"" + `) +
+					this.chunks
 						.map((chunk: Node) => {
 							if (chunk.type === 'Text') {
 								return stringify(chunk.data);
 							} else {
-								const { indexes } = block.contextualise(chunk.expression);
-								const { dependencies, snippet } = chunk.metadata;
+								const { dependencies, snippet, indexes } = chunk;
 
 								if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
 									hasChangeableIndex = true;
@@ -121,7 +138,7 @@ export default class Attribute extends Node {
 									allDependencies.add(d);
 								});
 
-								return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
+								return getExpressionPrecedence(chunk) <= 13 ? `(${snippet})` : snippet;
 							}
 						})
 						.join(' + ');
@@ -211,9 +228,9 @@ export default class Attribute extends Node {
 				);
 			}
 		} else {
-			const value = this.value === true
+			const value = this.isTrue
 				? 'true'
-				: this.value.length === 0 ? `""` : stringify(this.value[0].data);
+				: this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data);
 
 			const statement = (
 				isLegacyInputType
@@ -237,7 +254,7 @@ export default class Attribute extends Node {
 			const updateValue = `${node.var}.value = ${node.var}.__value;`;
 
 			block.builders.hydrate.addLine(updateValue);
-			if (isDynamic) block.builders.update.addLine(updateValue);
+			if (this.isDynamic) block.builders.update.addLine(updateValue);
 		}
 	}
 
@@ -260,8 +277,7 @@ export default class Attribute extends Node {
 							if (chunk.type === 'Text') {
 								return stringify(chunk.data);
 							} else {
-								const { indexes } = block.contextualise(chunk.expression);
-								const { dependencies, snippet } = chunk.metadata;
+								const { dependencies, snippet, indexes } = chunk;
 
 								if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
 									hasChangeableIndex = true;
@@ -297,12 +313,6 @@ export default class Attribute extends Node {
 			);
 		});
 	}
-
-	isDynamic() {
-		if (this.value === true || this.value.length === 0) return false;
-		if (this.value.length > 1) return true;
-		return this.value[0].type !== 'Text';
-	}
 }
 
 // source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
diff --git a/src/generators/nodes/Binding.ts b/src/generators/nodes/Binding.ts
index 1ac182102b..4a452d6ee6 100644
--- a/src/generators/nodes/Binding.ts
+++ b/src/generators/nodes/Binding.ts
@@ -5,6 +5,7 @@ import getTailSnippet from '../../utils/getTailSnippet';
 import flattenReference from '../../utils/flattenReference';
 import { DomGenerator } from '../dom/index';
 import Block from '../dom/Block';
+import Expression from './shared/Expression';
 
 const readOnlyMediaAttributes = new Set([
 	'duration',
@@ -15,8 +16,14 @@ const readOnlyMediaAttributes = new Set([
 
 export default class Binding extends Node {
 	name: string;
-	value: Node;
-	expression: Node;
+	value: Expression;
+
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+
+		this.name = info.name;
+		this.value = new Expression(compiler, this, info.value);
+	}
 
 	munge(
 		block: Block,
@@ -29,21 +36,20 @@ export default class Binding extends Node {
 
 		let updateCondition: string;
 
-		const { name } = getObject(this.value);
-		const { contexts } = block.contextualise(this.value);
-		const { snippet } = this.metadata;
+		const { name } = getObject(this.value.node);
+		const { contexts, snippet } = this.value;
 
 		// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
 		// and `selected` is an object chosen with a <select>, then when `checked` changes,
 		// we need to tell the component to update all the values `selected` might be
 		// pointing to
 		// TODO should this happen in preprocess?
-		const dependencies = this.metadata.dependencies.slice();
-		this.metadata.dependencies.forEach((prop: string) => {
-			const indirectDependencies = this.generator.indirectDependencies.get(prop);
+		const dependencies = new Set(this.value.dependencies);
+		this.value.dependencies.forEach((prop: string) => {
+			const indirectDependencies = this.compiler.indirectDependencies.get(prop);
 			if (indirectDependencies) {
 				indirectDependencies.forEach(indirectDependency => {
-					if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
+					dependencies.add(indirectDependency);
 				});
 			}
 		});
@@ -53,8 +59,8 @@ export default class Binding extends Node {
 		});
 
 		// view to model
-		const valueFromDom = getValueFromDom(this.generator, node, this);
-		const handler = getEventHandler(this.generator, block, name, snippet, this, dependencies, valueFromDom);
+		const valueFromDom = getValueFromDom(this.compiler, node, this);
+		const handler = getEventHandler(this.compiler, block, name, snippet, this, dependencies, valueFromDom);
 
 		// model to view
 		let updateDom = getDomUpdater(node, this, snippet);
@@ -62,7 +68,7 @@ export default class Binding extends Node {
 
 		// special cases
 		if (this.name === 'group') {
-			const bindingGroup = getBindingGroup(this.generator, this.value);
+			const bindingGroup = getBindingGroup(this.compiler, this.value);
 
 			block.builders.hydrate.addLine(
 				`#component._bindingGroups[${bindingGroup}].push(${node.var});`
@@ -135,23 +141,23 @@ function getDomUpdater(
 	return `${node.var}.${binding.name} = ${snippet};`;
 }
 
-function getBindingGroup(generator: DomGenerator, value: Node) {
+function getBindingGroup(compiler: DomGenerator, value: Node) {
 	const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
 	const keypath = parts.join('.');
 
 	// TODO handle contextual bindings — `keypath` should include unique ID of
 	// each block that provides context
-	let index = generator.bindingGroups.indexOf(keypath);
+	let index = compiler.bindingGroups.indexOf(keypath);
 	if (index === -1) {
-		index = generator.bindingGroups.length;
-		generator.bindingGroups.push(keypath);
+		index = compiler.bindingGroups.length;
+		compiler.bindingGroups.push(keypath);
 	}
 
 	return index;
 }
 
 function getEventHandler(
-	generator: DomGenerator,
+	compiler: DomGenerator,
 	block: Block,
 	name: string,
 	snippet: string,
@@ -159,8 +165,8 @@ function getEventHandler(
 	dependencies: string[],
 	value: string,
 ) {
-	const storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
-	dependencies = dependencies.filter(prop => prop[0] !== '$');
+	const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
+	dependencies = [...dependencies].filter(prop => prop[0] !== '$');
 
 	if (block.contexts.has(name)) {
 		const tail = attribute.value.type === 'MemberExpression'
@@ -186,9 +192,9 @@ function getEventHandler(
 		// Svelte tries to `set()` a computed property, which throws an
 		// error in dev mode. a) it's possible that we should be
 		// replacing computations with *their* dependencies, and b)
-		// we should probably populate `generator.readonly` sooner so
+		// we should probably populate `compiler.readonly` sooner so
 		// that we don't have to do the `.some()` here
-		dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
+		dependencies = dependencies.filter(prop => !compiler.computations.some(computation => computation.key === prop));
 
 		return {
 			usesContext: false,
@@ -222,7 +228,7 @@ function getEventHandler(
 }
 
 function getValueFromDom(
-	generator: DomGenerator,
+	compiler: DomGenerator,
 	node: Element,
 	binding: Node
 ) {
@@ -237,7 +243,7 @@ function getValueFromDom(
 
 	// <input type='checkbox' bind:group='foo'>
 	if (binding.name === 'group') {
-		const bindingGroup = getBindingGroup(generator, binding.value);
+		const bindingGroup = getBindingGroup(compiler, binding.value);
 		if (type === 'checkbox') {
 			return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
 		}
diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts
index a1861664eb..d5512429a4 100644
--- a/src/generators/nodes/Component.ts
+++ b/src/generators/nodes/Component.ts
@@ -11,6 +11,7 @@ import Node from './shared/Node';
 import Block from '../dom/Block';
 import Attribute from './Attribute';
 import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments';
+import mapChildren from './shared/mapChildren';
 
 export default class Component extends Node {
 	type: 'Component';
@@ -18,6 +19,31 @@ export default class Component extends Node {
 	attributes: Attribute[];
 	children: Node[];
 
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+
+		compiler.hasComponents = true;
+
+		this.name = info.name;
+
+		this.attributes = [];
+		// TODO bindings etc
+
+		info.attributes.forEach(node => {
+			switch (node.type) {
+				case 'Attribute':
+					// TODO spread
+					this.attributes.push(new Attribute(compiler, this, node));
+					break;
+
+				default:
+					throw new Error(`Not implemented: ${node.type}`);
+			}
+		});
+
+		this.children = mapChildren(compiler, this, info.children);
+	}
+
 	init(
 		block: Block,
 		stripWhitespace: boolean,
@@ -46,7 +72,7 @@ export default class Component extends Node {
 
 		this.var = block.getUniqueName(
 			(
-				this.name === 'svelte:self' ? this.generator.name :
+				this.name === 'svelte:self' ? this.compiler.name :
 				this.name === 'svelte:component' ? 'switch_instance' :
 				this.name
 			).toLowerCase()
@@ -66,8 +92,7 @@ export default class Component extends Node {
 		parentNode: string,
 		parentNodes: string
 	) {
-		const { generator } = this;
-		generator.hasComponents = true;
+		const { compiler } = this;
 
 		const name = this.var;
 
@@ -100,10 +125,10 @@ export default class Component extends Node {
 
 		const eventHandlers = this.attributes
 			.filter((a: Node) => a.type === 'EventHandler')
-			.map(a => mungeEventHandler(generator, this, a, block, allContexts));
+			.map(a => mungeEventHandler(compiler, this, a, block, allContexts));
 
 		const ref = this.attributes.find((a: Node) => a.type === 'Ref');
-		if (ref) generator.usesRefs = true;
+		if (ref) compiler.usesRefs = true;
 
 		const updates: string[] = [];
 
@@ -187,7 +212,7 @@ export default class Component extends Node {
 		}
 
 		if (bindings.length) {
-			generator.hasComplexBindings = true;
+			compiler.hasComplexBindings = true;
 
 			name_updating = block.alias(`${name}_updating`);
 			block.addVariable(name_updating, '{}');
@@ -389,7 +414,7 @@ export default class Component extends Node {
 			block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
 		} else {
 			const expression = this.name === 'svelte:self'
-				? generator.name
+				? compiler.name
 				: `%components-${this.name}`;
 
 			block.builders.init.addBlock(deindent`
@@ -478,18 +503,18 @@ function mungeBinding(binding: Node, block: Block): Binding {
 	};
 }
 
-function mungeEventHandler(generator: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set<string>) {
+function mungeEventHandler(compiler: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set<string>) {
 	let body;
 
 	if (handler.expression) {
-		generator.addSourcemapLocations(handler.expression);
+		compiler.addSourcemapLocations(handler.expression);
 
 		// TODO try out repetition between this and element counterpart
 		const flattened = flattenReference(handler.expression.callee);
 			if (!validCalleeObjects.has(flattened.name)) {
 				// allow event.stopPropagation(), this.select() etc
 				// TODO verify that it's a valid callee (i.e. built-in or declared method)
-				generator.code.prependRight(
+				compiler.code.prependRight(
 					handler.expression.start,
 					`${block.alias('component')}.`
 				);
diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts
index e74b8a01c2..3d7ca27469 100644
--- a/src/generators/nodes/EachBlock.ts
+++ b/src/generators/nodes/EachBlock.ts
@@ -3,12 +3,14 @@ import Node from './shared/Node';
 import ElseBlock from './ElseBlock';
 import Block from '../dom/Block';
 import createDebuggingComment from '../../utils/createDebuggingComment';
+import Expression from './shared/Expression';
+import mapChildren from './shared/mapChildren';
 
 export default class EachBlock extends Node {
 	type: 'EachBlock';
 
 	block: Block;
-	expression: Node;
+	expression: Expression;
 
 	iterations: string;
 	index: string;
@@ -19,6 +21,16 @@ export default class EachBlock extends Node {
 	children: Node[];
 	else?: ElseBlock;
 
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+
+		this.expression = new Expression(compiler, this, info.expression);
+		this.context = info.context;
+		this.key = info.key;
+
+		this.children = mapChildren(compiler, this, info.children);
+	}
+
 	init(
 		block: Block,
 		stripWhitespace: boolean,
@@ -30,12 +42,12 @@ export default class EachBlock extends Node {
 		this.iterations = block.getUniqueName(`${this.var}_blocks`);
 		this.each_context = block.getUniqueName(`${this.var}_context`);
 
-		const { dependencies } = this.metadata;
+		const { dependencies } = this.expression;
 		block.addDependencies(dependencies);
 
 		this.block = block.child({
-			comment: createDebuggingComment(this, this.generator),
-			name: this.generator.getUniqueName('create_each_block'),
+			comment: createDebuggingComment(this, this.compiler),
+			name: this.compiler.getUniqueName('create_each_block'),
 			context: this.context,
 			key: this.key,
 
@@ -48,8 +60,8 @@ export default class EachBlock extends Node {
 			listNames: new Map(block.listNames)
 		});
 
-		const listName = this.generator.getUniqueName('each_value');
-		const indexName = this.index || this.generator.getUniqueName(`${this.context}_index`);
+		const listName = this.compiler.getUniqueName('each_value');
+		const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);
 
 		this.block.contextTypes.set(this.context, 'each');
 		this.block.indexNames.set(this.context, indexName);
@@ -83,18 +95,18 @@ export default class EachBlock extends Node {
 			}
 		}
 
-		this.generator.blocks.push(this.block);
+		this.compiler.blocks.push(this.block);
 		this.initChildren(this.block, stripWhitespace, nextSibling);
 		block.addDependencies(this.block.dependencies);
 		this.block.hasUpdateMethod = this.block.dependencies.size > 0;
 
 		if (this.else) {
 			this.else.block = block.child({
-				comment: createDebuggingComment(this.else, this.generator),
-				name: this.generator.getUniqueName(`${this.block.name}_else`),
+				comment: createDebuggingComment(this.else, this.compiler),
+				name: this.compiler.getUniqueName(`${this.block.name}_else`),
 			});
 
-			this.generator.blocks.push(this.else.block);
+			this.compiler.blocks.push(this.else.block);
 			this.else.initChildren(
 				this.else.block,
 				stripWhitespace,
@@ -111,7 +123,7 @@ export default class EachBlock extends Node {
 	) {
 		if (this.children.length === 0) return;
 
-		const { generator } = this;
+		const { compiler } = this;
 
 		const each = this.var;
 
@@ -127,8 +139,8 @@ export default class EachBlock extends Node {
 		// hack the sourcemap, so that if data is missing the bug
 		// is easy to find
 		let c = this.start + 2;
-		while (generator.source[c] !== 'e') c += 1;
-		generator.code.overwrite(c, c + 4, 'length');
+		while (compiler.source[c] !== 'e') c += 1;
+		compiler.code.overwrite(c, c + 4, 'length');
 		const length = `[✂${c}-${c+4}✂]`;
 
 		const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
@@ -142,8 +154,7 @@ export default class EachBlock extends Node {
 			mountOrIntro,
 		};
 
-		block.contextualise(this.expression);
-		const { snippet } = this.metadata;
+		const { snippet } = this.expression;
 
 		block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
 
@@ -163,14 +174,14 @@ export default class EachBlock extends Node {
 		}
 
 		if (this.else) {
-			const each_block_else = generator.getUniqueName(`${each}_else`);
+			const each_block_else = compiler.getUniqueName(`${each}_else`);
 
 			block.builders.init.addLine(`var ${each_block_else} = null;`);
 
 			// TODO neaten this up... will end up with an empty line in the block
 			block.builders.init.addBlock(deindent`
 				if (!${each_block_value}.${length}) {
-					${each_block_else} = ${this.else.block.name}(#component, state);
+					${each_block_else} = ${this.else.block.name}(#component, ctx);
 					${each_block_else}.c();
 				}
 			`);
@@ -186,9 +197,9 @@ export default class EachBlock extends Node {
 			if (this.else.block.hasUpdateMethod) {
 				block.builders.update.addBlock(deindent`
 					if (!${each_block_value}.${length} && ${each_block_else}) {
-						${each_block_else}.p(changed, state);
+						${each_block_else}.p(changed, ctx);
 					} else if (!${each_block_value}.${length}) {
-						${each_block_else} = ${this.else.block.name}(#component, state);
+						${each_block_else} = ${this.else.block.name}(#component, ctx);
 						${each_block_else}.c();
 						${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
 					} else if (${each_block_else}) {
@@ -206,7 +217,7 @@ export default class EachBlock extends Node {
 							${each_block_else} = null;
 						}
 					} else if (!${each_block_else}) {
-						${each_block_else} = ${this.else.block.name}(#component, state);
+						${each_block_else} = ${this.else.block.name}(#component, ctx);
 						${each_block_else}.c();
 						${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
 					}
@@ -269,7 +280,7 @@ export default class EachBlock extends Node {
 		block.builders.init.addBlock(deindent`
 			for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
 				var ${key} = ${each_block_value}[#i].${this.key};
-				${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, state), {
+				${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), {
 					${this.contextProps.join(',\n')}
 				}));
 			}
@@ -299,7 +310,7 @@ export default class EachBlock extends Node {
 			var ${each_block_value} = ${snippet};
 
 			${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
-				return @assign(@assign({}, state), {
+				return @assign(@assign({}, ctx), {
 					${this.contextProps.join(',\n')}
 				});
 			});
@@ -334,7 +345,7 @@ export default class EachBlock extends Node {
 			var ${iterations} = [];
 
 			for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
-				${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, state), {
+				${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, ctx), {
 					${this.contextProps.join(',\n')}
 				}));
 			}
@@ -365,7 +376,7 @@ export default class EachBlock extends Node {
 		`);
 
 		const allDependencies = new Set(this.block.dependencies);
-		const { dependencies } = this.metadata;
+		const { dependencies } = this.expression;
 		dependencies.forEach((dependency: string) => {
 			allDependencies.add(dependency);
 		});
@@ -432,7 +443,7 @@ export default class EachBlock extends Node {
 
 				if (${condition}) {
 					for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
-						var ${this.each_context} = @assign(@assign({}, state), {
+						var ${this.each_context} = @assign(@assign({}, ctx), {
 							${this.contextProps.join(',\n')}
 						});
 
diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts
index eb6c01e351..7e848a4c1e 100644
--- a/src/generators/nodes/Element.ts
+++ b/src/generators/nodes/Element.ts
@@ -22,30 +22,71 @@ import mapChildren from './shared/mapChildren';
 export default class Element extends Node {
 	type: 'Element';
 	name: string;
-	attributes: (Attribute | Binding | EventHandler | Ref | Transition | Action)[]; // TODO split these up sooner
+	attributes: Attribute[];
+	bindings: Binding[];
+	handlers: EventHandler[];
+	intro: Transition;
+	outro: Transition;
 	children: Node[];
 
+	ref: string;
+	namespace: string;
+
 	constructor(compiler, parent, info: any) {
 		super(compiler, parent, info);
 		this.name = info.name;
-		this.children = mapChildren(compiler, parent, info.children);
+
+		const parentElement = parent.findNearest(/^Element/);
+		this.namespace = this.name === 'svg' ?
+			namespaces.svg :
+			parentElement ? parentElement.namespace : this.compiler.namespace;
 
 		this.attributes = [];
-		// TODO bindings etc
+		this.bindings = [];
+		this.handlers = [];
+
+		this.intro = null;
+		this.outro = null;
 
 		info.attributes.forEach(node => {
 			switch (node.type) {
 				case 'Attribute':
-				case 'Spread':
+					// special case
+					if (node.name === 'xmlns') this.namespace = node.value[0].data;
+
 					this.attributes.push(new Attribute(compiler, this, node));
 					break;
 
+				case 'Binding':
+					this.bindings.push(new Binding(compiler, this, node));
+					break;
+
+				case 'EventHandler':
+					this.handlers.push(new EventHandler(compiler, this, node));
+					break;
+
+				case 'Transition':
+					const transition = new Transition(compiler, this, node);
+					if (node.intro) this.intro = transition;
+					if (node.outro) this.outro = transition;
+					break;
+
+				case 'Ref':
+					// TODO catch this in validation
+					if (this.ref) throw new Error(`Duplicate refs`);
+
+					compiler.usesRefs = true
+					this.ref = node.name;
+					break;
+
 				default:
 					throw new Error(`Not implemented: ${node.type}`);
 			}
 		});
 
 		// TODO break out attributes and directives here
+
+		this.children = mapChildren(compiler, this, info.children);
 	}
 
 	init(
@@ -57,61 +98,53 @@ export default class Element extends Node {
 			this.cannotUseInnerHTML();
 		}
 
-		const parentElement = this.parent && this.parent.findNearest(/^Element/);
-		this.namespace = this.name === 'svg' ?
-			namespaces.svg :
-			parentElement ? parentElement.namespace : this.generator.namespace;
+		this.attributes.forEach(attr => {
+			if (attr.dependencies.size) {
+				this.parent.cannotUseInnerHTML();
+				block.addDependencies(attr.dependencies);
+
+				// special case — <option value={foo}> — see below
+				if (this.name === 'option' && attr.name === 'value') {
+					let select = this.parent;
+					while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
+
+					if (select && select.selectBindingDependencies) {
+						select.selectBindingDependencies.forEach(prop => {
+							dependencies.forEach((dependency: string) => {
+								this.compiler.indirectDependencies.get(prop).add(dependency);
+							});
+						});
+					}
+				}
+			}
+		});
+
+		this.bindings.forEach(binding => {
+			this.cannotUseInnerHTML();
+			block.addDependencies(binding.value.dependencies);
+		});
+
+		this.handlers.forEach(handler => {
+			this.cannotUseInnerHTML();
+			block.addDependencies(handler.dependencies);
+		});
+
+		if (this.intro) {
+			this.compiler.hasIntroTransitions = block.hasIntroMethod = true;
+		}
+
+		if (this.outro) {
+			this.compiler.hasOutroTransitions = block.hasOutroMethod = true;
+			block.outros += 1;
+		}
 
 		this.attributes.forEach(attribute => {
 			if (attribute.type === 'Attribute' && attribute.value !== true) {
-				// special case — xmlns
-				if (attribute.name === 'xmlns') {
-					// TODO this attribute must be static – enforce at compile time
-					this.namespace = attribute.value[0].data;
-				}
-
-				attribute.value.forEach((chunk: Node) => {
-					if (chunk.type !== 'Text') {
-						if (this.parent) this.parent.cannotUseInnerHTML();
-
-						const dependencies = chunk.metadata.dependencies;
-						block.addDependencies(dependencies);
-
-						// special case — <option value='{{foo}}'> — see below
-						if (
-							this.name === 'option' &&
-							attribute.name === 'value'
-						) {
-							let select = this.parent;
-							while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
-
-							if (select && select.selectBindingDependencies) {
-								select.selectBindingDependencies.forEach(prop => {
-									dependencies.forEach((dependency: string) => {
-										this.generator.indirectDependencies.get(prop).add(dependency);
-									});
-								});
-							}
-						}
-					}
-				});
+				// removed
 			} else {
 				if (this.parent) this.parent.cannotUseInnerHTML();
 
-				if (attribute.type === 'EventHandler' && attribute.expression) {
-					attribute.expression.arguments.forEach((arg: Node) => {
-						block.addDependencies(arg.metadata.dependencies);
-					});
-				} else if (attribute.type === 'Binding') {
-					block.addDependencies(attribute.metadata.dependencies);
-				} else if (attribute.type === 'Transition') {
-					if (attribute.intro)
-						this.generator.hasIntroTransitions = block.hasIntroMethod = true;
-					if (attribute.outro) {
-						this.generator.hasOutroTransitions = block.hasOutroMethod = true;
-						block.outros += 1;
-					}
-				} else if (attribute.type === 'Action' && attribute.expression) {
+				if (attribute.type === 'Action' && attribute.expression) {
 					block.addDependencies(attribute.metadata.dependencies);
 				} else if (attribute.type === 'Spread') {
 					block.addDependencies(attribute.metadata.dependencies);
@@ -125,11 +158,9 @@ export default class Element extends Node {
 			// this is an egregious hack, but it's the easiest way to get <textarea>
 			// children treated the same way as a value attribute
 			if (this.children.length > 0) {
-				this.attributes.push(new Attribute({
-					generator: this.generator,
+				this.attributes.push(new Attribute(this.compiler, this, {
 					name: 'value',
-					value: this.children,
-					parent: this
+					value: this.children
 				}));
 
 				this.children = [];
@@ -153,7 +184,7 @@ export default class Element extends Node {
 				const dependencies = binding.metadata.dependencies;
 				this.selectBindingDependencies = dependencies;
 				dependencies.forEach((prop: string) => {
-					this.generator.indirectDependencies.set(prop, new Set());
+					this.compiler.indirectDependencies.set(prop, new Set());
 				});
 			} else {
 				this.selectBindingDependencies = null;
@@ -188,11 +219,11 @@ export default class Element extends Node {
 		parentNode: string,
 		parentNodes: string
 	) {
-		const { generator } = this;
+		const { compiler } = this;
 
 		if (this.name === 'slot') {
 			const slotName = this.getStaticAttributeValue('name') || 'default';
-			this.generator.slots.add(slotName);
+			this.compiler.slots.add(slotName);
 		}
 
 		if (this.name === 'noscript') return;
@@ -211,15 +242,15 @@ export default class Element extends Node {
 			parentNode;
 
 		block.addVariable(name);
-		const renderStatement = getRenderStatement(this.generator, this.namespace, this.name);
+		const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name);
 		block.builders.create.addLine(
 			`${name} = ${renderStatement};`
 		);
 
-		if (this.generator.hydratable) {
+		if (this.compiler.hydratable) {
 			if (parentNodes) {
 				block.builders.claim.addBlock(deindent`
-					${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)};
+					${name} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
 					var ${childState.parentNodes} = @children(${name});
 				`);
 			} else {
@@ -271,7 +302,7 @@ export default class Element extends Node {
 
 		this.addBindings(block, allUsedContexts);
 		const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
-		this.addRefs(block);
+		if (this.ref) this.addRef(block);
 		this.addAttributes(block);
 		this.addTransitions(block);
 		this.addActions(block);
@@ -353,14 +384,14 @@ export default class Element extends Node {
 		block: Block,
 		allUsedContexts: Set<string>
 	) {
-		const bindings: Binding[] = this.attributes.filter((a: Binding) => a.type === 'Binding');
-		if (bindings.length === 0) return;
+		if (this.bindings.length === 0) return;
 
-		if (this.name === 'select' || this.isMediaNode()) this.generator.hasComplexBindings = true;
+		if (this.name === 'select' || this.isMediaNode()) this.compiler.hasComplexBindings = true;
 
 		const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
 
-		const mungedBindings = bindings.map(binding => binding.munge(block, allUsedContexts));
+		// TODO munge in constructor
+		const mungedBindings = this.bindings.map(binding => binding.munge(block, allUsedContexts));
 
 		const lock = mungedBindings.some(binding => binding.needsLock) ?
 			block.getUniqueName(`${this.var}_updating`) :
@@ -452,7 +483,7 @@ export default class Element extends Node {
 				.join(' && ');
 
 			if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
-				this.generator.hasComplexBindings = true;
+				this.compiler.hasComplexBindings = true;
 
 				block.builders.hydrate.addLine(
 					`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
@@ -469,7 +500,7 @@ export default class Element extends Node {
 			return;
 		}
 
-		this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
+		this.attributes.forEach((attribute: Attribute) => {
 			attribute.render(block);
 		});
 	}
@@ -528,89 +559,58 @@ export default class Element extends Node {
 	}
 
 	addEventHandlers(block: Block, allUsedContexts) {
-		const { generator } = this;
+		const { compiler } = this;
 		let eventHandlerUsesComponent = false;
 
-		this.attributes.filter((a: EventHandler) => a.type === 'EventHandler').forEach((attribute: EventHandler) => {
-			const isCustomEvent = generator.events.has(attribute.name);
+		this.handlers.forEach(handler => {
+			const isCustomEvent = compiler.events.has(handler.name);
 			const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
 
 			const context = shouldHoist ? null : this.var;
 			const usedContexts: string[] = [];
 
-			if (attribute.expression) {
-				generator.addSourcemapLocations(attribute.expression);
-
-				const flattened = flattenReference(attribute.expression.callee);
-				if (!validCalleeObjects.has(flattened.name)) {
-					// allow event.stopPropagation(), this.select() etc
-					// TODO verify that it's a valid callee (i.e. built-in or declared method)
-					if (flattened.name[0] === '$' && !generator.methods.has(flattened.name)) {
-						generator.code.overwrite(
-							attribute.expression.start,
-							attribute.expression.start + 1,
-							`${block.alias('component')}.store.`
-						);
-					} else {
-						generator.code.prependRight(
-							attribute.expression.start,
-							`${block.alias('component')}.`
-						);
-					}
+			if (handler.callee) {
+				handler.render(this.compiler, block);
 
+				if (!validCalleeObjects.has(handler.callee.name)) {
 					if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
 				}
 
-				attribute.expression.arguments.forEach((arg: Node) => {
-					const { contexts } = block.contextualise(arg, context, true);
+				// handler.expression.arguments.forEach((arg: Node) => {
+				// 	const { contexts } = block.contextualise(arg, context, true);
 
-					contexts.forEach(context => {
-						if (!~usedContexts.indexOf(context)) usedContexts.push(context);
-						allUsedContexts.add(context);
-					});
-				});
+				// 	contexts.forEach(context => {
+				// 		if (!~usedContexts.indexOf(context)) usedContexts.push(context);
+				// 		allUsedContexts.add(context);
+				// 	});
+				// });
 			}
 
 			const ctx = context || 'this';
-			const declarations = usedContexts
-				.map(name => {
-					if (name === 'state') {
-						if (shouldHoist) eventHandlerUsesComponent = true;
-						return `var state = ${block.alias('component')}.get();`;
-					}
-
-					const contextType = block.contextTypes.get(name);
-					if (contextType === 'each') {
-						const listName = block.listNames.get(name);
-						const indexName = block.indexNames.get(name);
-						const contextName = block.contexts.get(name);
-
-						return `var ${listName} = ${ctx}._svelte.${listName}, ${indexName} = ${ctx}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
-					}
-				})
-				.filter(Boolean);
 
 			// get a name for the event handler that is globally unique
 			// if hoisted, locally unique otherwise
-			const handlerName = (shouldHoist ? generator : block).getUniqueName(
-				`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
+			const handlerName = (shouldHoist ? compiler : block).getUniqueName(
+				`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
 			);
 
+			const component = block.alias('component'); // can't use #component, might be hoisted
+
 			// create the handler body
 			const handlerBody = deindent`
 				${eventHandlerUsesComponent &&
-					`var ${block.alias('component')} = ${ctx}._svelte.component;`}
-				${declarations}
-				${attribute.expression ?
-					`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
-					`${block.alias('component')}.fire("${attribute.name}", event);`}
+					`var #component = ${ctx}._svelte.component;`}
+				${handler.dependencies.size > 0 && `const ctx = #component.get();`}
+				${handler.snippet ?
+					handler.snippet :
+					`#component.fire("${handler.name}", event);`}
 			`;
 
 			if (isCustomEvent) {
 				block.addVariable(handlerName);
 
 				block.builders.hydrate.addBlock(deindent`
-					${handlerName} = %events-${attribute.name}.call(#component, ${this.var}, function(event) {
+					${handlerName} = %events-${handler.name}.call(#component, ${this.var}, function(event) {
 						${handlerBody}
 					});
 				`);
@@ -619,61 +619,53 @@ export default class Element extends Node {
 					${handlerName}.destroy();
 				`);
 			} else {
-				const handler = deindent`
+				const handlerFunction = deindent`
 					function ${handlerName}(event) {
 						${handlerBody}
 					}
 				`;
 
 				if (shouldHoist) {
-					generator.blocks.push(handler);
+					compiler.blocks.push(handlerFunction);
 				} else {
-					block.builders.init.addBlock(handler);
+					block.builders.init.addBlock(handlerFunction);
 				}
 
 				block.builders.hydrate.addLine(
-					`@addListener(${this.var}, "${attribute.name}", ${handlerName});`
+					`@addListener(${this.var}, "${handler.name}", ${handlerName});`
 				);
 
 				block.builders.destroy.addLine(
-					`@removeListener(${this.var}, "${attribute.name}", ${handlerName});`
+					`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
 				);
 			}
 		});
 		return eventHandlerUsesComponent;
 	}
 
-	addRefs(block: Block) {
-		// TODO it should surely be an error to have more than one ref
-		this.attributes.filter((a: Ref) => a.type === 'Ref').forEach((attribute: Ref) => {
-			const ref = `#component.refs.${attribute.name}`;
+	addRef(block: Block) {
+		const ref = `#component.refs.${this.ref}`;
 
-			block.builders.mount.addLine(
-				`${ref} = ${this.var};`
-			);
-
-			block.builders.destroy.addLine(
-				`if (${ref} === ${this.var}) ${ref} = null;`
-			);
+		block.builders.mount.addLine(
+			`${ref} = ${this.var};`
+		);
 
-			this.generator.usesRefs = true; // so component.refs object is created
-		});
+		block.builders.destroy.addLine(
+			`if (${ref} === ${this.var}) ${ref} = null;`
+		);
 	}
 
 	addTransitions(
 		block: Block
 	) {
-		const intro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.intro);
-		const outro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.outro);
+		const { intro, outro } = this;
 
 		if (!intro && !outro) return;
 
 		if (intro === outro) {
-			block.contextualise(intro.expression); // TODO remove all these
-
 			const name = block.getUniqueName(`${this.var}_transition`);
 			const snippet = intro.expression
-				? intro.metadata.snippet
+				? intro.expression.snippet
 				: '{}';
 
 			block.addVariable(name);
@@ -701,11 +693,9 @@ export default class Element extends Node {
 			const outroName = outro && block.getUniqueName(`${this.var}_outro`);
 
 			if (intro) {
-				block.contextualise(intro.expression);
-
 				block.addVariable(introName);
 				const snippet = intro.expression
-					? intro.metadata.snippet
+					? intro.expression.snippet
 					: '{}';
 
 				const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
@@ -728,11 +718,9 @@ export default class Element extends Node {
 			}
 
 			if (outro) {
-				block.contextualise(outro.expression);
-
 				block.addVariable(outroName);
 				const snippet = outro.expression
-					? outro.metadata.snippet
+					? outro.expression.snippet
 					: '{}';
 
 				const fn = `%transitions-${outro.name}`;
@@ -755,7 +743,7 @@ export default class Element extends Node {
 			const { expression } = attribute;
 			let snippet, dependencies;
 			if (expression) {
-				this.generator.addSourcemapLocations(expression);
+				this.compiler.addSourcemapLocations(expression);
 				block.contextualise(expression);
 				snippet = attribute.metadata.snippet;
 				dependencies = attribute.metadata.dependencies;
@@ -823,18 +811,18 @@ export default class Element extends Node {
 		const classAttribute = this.attributes.find(a => a.name === 'class');
 		if (classAttribute && classAttribute.value !== true) {
 			if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') {
-				classAttribute.value[0].data += ` ${this.generator.stylesheet.id}`;
+				classAttribute.value[0].data += ` ${this.compiler.stylesheet.id}`;
 			} else {
 				(<Node[]>classAttribute.value).push(
-					new Node({ type: 'Text', data: ` ${this.generator.stylesheet.id}` })
+					new Node({ type: 'Text', data: ` ${this.compiler.stylesheet.id}` })
 				);
 			}
 		} else {
 			this.attributes.push(
 				new Attribute({
-					generator: this.generator,
+					compiler: this.compiler,
 					name: 'class',
-					value: [new Node({ type: 'Text', data: `${this.generator.stylesheet.id}` })],
+					value: [new Node({ type: 'Text', data: `${this.compiler.stylesheet.id}` })],
 					parent: this,
 				})
 			);
@@ -843,7 +831,7 @@ export default class Element extends Node {
 }
 
 function getRenderStatement(
-	generator: DomGenerator,
+	compiler: DomGenerator,
 	namespace: string,
 	name: string
 ) {
@@ -859,7 +847,7 @@ function getRenderStatement(
 }
 
 function getClaimStatement(
-	generator: DomGenerator,
+	compiler: DomGenerator,
 	namespace: string,
 	nodes: string,
 	node: Node
diff --git a/src/generators/nodes/ElseBlock.ts b/src/generators/nodes/ElseBlock.ts
index 0a48881fbb..917e3aa026 100644
--- a/src/generators/nodes/ElseBlock.ts
+++ b/src/generators/nodes/ElseBlock.ts
@@ -1,8 +1,13 @@
 import Node from './shared/Node';
 import Block from '../dom/Block';
+import mapChildren from './shared/mapChildren';
 
 export default class ElseBlock extends Node {
 	type: 'ElseBlock';
 	children: Node[];
-	block: Block;
+
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+		this.children = mapChildren(compiler, this, info.children);
+	}
 }
\ No newline at end of file
diff --git a/src/generators/nodes/EventHandler.ts b/src/generators/nodes/EventHandler.ts
index fae628e58c..34bc200b3d 100644
--- a/src/generators/nodes/EventHandler.ts
+++ b/src/generators/nodes/EventHandler.ts
@@ -1,7 +1,61 @@
 import Node from './shared/Node';
+import Expression from './shared/Expression';
+import addToSet from '../../utils/addToSet';
+import flattenReference from '../../utils/flattenReference';
+import validCalleeObjects from '../../utils/validCalleeObjects';
 
 export default class EventHandler extends Node {
 	name: string;
-	value: Node[]
-	expression: Node
+	dependencies: Set<string>;
+	expression: Node;
+	callee: any; // TODO
+	insertionPoint: number;
+	args: Expression[];
+	snippet: string;
+
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+
+		this.name = info.name;
+		this.dependencies = new Set();
+
+		if (info.expression) {
+			this.callee = flattenReference(info.expression.callee);
+			this.insertionPoint = info.expression.start;
+			this.args = info.expression.arguments.map(param => {
+				const expression = new Expression(compiler, this, param);
+				addToSet(this.dependencies, expression.dependencies);
+				return expression;
+			});
+
+			this.snippet = `[✂${info.expression.start}-${info.expression.end}✂]`;
+		} else {
+			this.callee = null;
+			this.insertionPoint = null;
+			this.args = null;
+
+			this.snippet = null; // TODO handle shorthand events here?
+		}
+	}
+
+	render(compiler, block) {
+		if (this.insertionPoint === null) return; // TODO handle shorthand events here?
+
+		if (!validCalleeObjects.has(this.callee.name)) {
+			// allow event.stopPropagation(), this.select() etc
+			// TODO verify that it's a valid callee (i.e. built-in or declared method)
+			if (this.callee.name[0] === '$' && !compiler.methods.has(this.callee.name)) {
+				compiler.code.overwrite(
+					this.insertionPoint,
+					this.insertionPoint + 1,
+					`${block.alias('component')}.store.`
+				);
+			} else {
+				compiler.code.prependRight(
+					this.insertionPoint,
+					`${block.alias('component')}.`
+				);
+			}
+		}
+	}
 }
\ No newline at end of file
diff --git a/src/generators/nodes/Fragment.ts b/src/generators/nodes/Fragment.ts
index f82b72b3d1..214c54b43d 100644
--- a/src/generators/nodes/Fragment.ts
+++ b/src/generators/nodes/Fragment.ts
@@ -9,13 +9,13 @@ export default class Fragment extends Node {
 	children: Node[];
 
 	constructor(compiler: Generator, info: any) {
-		super(compiler, info);
+		super(compiler, null, info);
 		this.children = mapChildren(compiler, this, info.children);
 	}
 
 	init() {
 		this.block = new Block({
-			generator: this.generator,
+			generator: this.compiler,
 			name: '@create_main_fragment',
 			key: null,
 
@@ -29,7 +29,7 @@ export default class Fragment extends Node {
 			dependencies: new Set(),
 		});
 
-		this.generator.blocks.push(this.block);
+		this.compiler.blocks.push(this.block);
 		this.initChildren(this.block, true, null);
 
 		this.block.hasUpdateMethod = true;
diff --git a/src/generators/nodes/IfBlock.ts b/src/generators/nodes/IfBlock.ts
index e519ca63f2..2167ea8baf 100644
--- a/src/generators/nodes/IfBlock.ts
+++ b/src/generators/nodes/IfBlock.ts
@@ -4,6 +4,8 @@ import ElseBlock from './ElseBlock';
 import { DomGenerator } from '../dom/index';
 import Block from '../dom/Block';
 import createDebuggingComment from '../../utils/createDebuggingComment';
+import Expression from './shared/Expression';
+import mapChildren from './shared/mapChildren';
 
 function isElseIf(node: ElseBlock) {
 	return (
@@ -17,16 +19,29 @@ function isElseBranch(branch) {
 
 export default class IfBlock extends Node {
 	type: 'IfBlock';
+	expression: Expression;
+	children: any[];
 	else: ElseBlock;
 
 	block: Block;
 
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+
+		this.expression = new Expression(compiler, this, info.expression);
+		this.children = mapChildren(compiler, this, info.children);
+
+		this.else = info.else
+			? new ElseBlock(compiler, this, info.else)
+			: null;
+	}
+
 	init(
 		block: Block,
 		stripWhitespace: boolean,
 		nextSibling: Node
 	) {
-		const { generator } = this;
+		const { compiler } = this;
 
 		this.cannotUseInnerHTML();
 
@@ -38,11 +53,11 @@ export default class IfBlock extends Node {
 		function attachBlocks(node: IfBlock) {
 			node.var = block.getUniqueName(`if_block`);
 
-			block.addDependencies(node.metadata.dependencies);
+			block.addDependencies(node.expression.dependencies);
 
 			node.block = block.child({
-				comment: createDebuggingComment(node, generator),
-				name: generator.getUniqueName(`create_if_block`),
+				comment: createDebuggingComment(node, compiler),
+				name: compiler.getUniqueName(`create_if_block`),
 			});
 
 			blocks.push(node.block);
@@ -60,8 +75,8 @@ export default class IfBlock extends Node {
 				attachBlocks(node.else.children[0]);
 			} else if (node.else) {
 				node.else.block = block.child({
-					comment: createDebuggingComment(node.else, generator),
-					name: generator.getUniqueName(`create_if_block`),
+					comment: createDebuggingComment(node.else, compiler),
+					name: compiler.getUniqueName(`create_if_block`),
 				});
 
 				blocks.push(node.else.block);
@@ -86,7 +101,7 @@ export default class IfBlock extends Node {
 			block.hasOutroMethod = hasOutros;
 		});
 
-		generator.blocks.push(...blocks);
+		compiler.blocks.push(...blocks);
 	}
 
 	build(
@@ -147,12 +162,12 @@ export default class IfBlock extends Node {
 		dynamic,
 		{ name, anchor, hasElse, if_name }
 	) {
-		const select_block_type = this.generator.getUniqueName(`select_block_type`);
+		const select_block_type = this.compiler.getUniqueName(`select_block_type`);
 		const current_block_type = block.getUniqueName(`current_block_type`);
 		const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
 
 		block.builders.init.addBlock(deindent`
-			function ${select_block_type}(state) {
+			function ${select_block_type}(ctx) {
 				${branches
 					.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`)
 					.join('\n')}
@@ -160,8 +175,8 @@ export default class IfBlock extends Node {
 		`);
 
 		block.builders.init.addBlock(deindent`
-			var ${current_block_type} = ${select_block_type}(state);
-			var ${name} = ${current_block_type_and}${current_block_type}(#component, state);
+			var ${current_block_type} = ${select_block_type}(ctx);
+			var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
 		`);
 
 		const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
@@ -185,22 +200,22 @@ export default class IfBlock extends Node {
 						${name}.u();
 						${name}.d();
 					}`}
-			${name} = ${current_block_type_and}${current_block_type}(#component, state);
+			${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
 			${if_name}${name}.c();
 			${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
 		`;
 
 		if (dynamic) {
 			block.builders.update.addBlock(deindent`
-				if (${current_block_type} === (${current_block_type} = ${select_block_type}(state)) && ${name}) {
-					${name}.p(changed, state);
+				if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
+					${name}.p(changed, ctx);
 				} else {
 					${changeBlock}
 				}
 			`);
 		} else {
 			block.builders.update.addBlock(deindent`
-				if (${current_block_type} !== (${current_block_type} = ${select_block_type}(state))) {
+				if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
 					${changeBlock}
 				}
 			`);
@@ -241,7 +256,7 @@ export default class IfBlock extends Node {
 
 			var ${if_blocks} = [];
 
-			function ${select_block_type}(state) {
+			function ${select_block_type}(ctx) {
 				${branches
 					.map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`)
 					.join('\n')}
@@ -250,13 +265,13 @@ export default class IfBlock extends Node {
 
 		if (hasElse) {
 			block.builders.init.addBlock(deindent`
-				${current_block_type_index} = ${select_block_type}(state);
-				${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state);
+				${current_block_type_index} = ${select_block_type}(ctx);
+				${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
 			`);
 		} else {
 			block.builders.init.addBlock(deindent`
-				if (~(${current_block_type_index} = ${select_block_type}(state))) {
-					${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state);
+				if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
+					${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
 				}
 			`);
 		}
@@ -282,7 +297,7 @@ export default class IfBlock extends Node {
 		const createNewBlock = deindent`
 			${name} = ${if_blocks}[${current_block_type_index}];
 			if (!${name}) {
-				${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state);
+				${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
 				${name}.c();
 			}
 			${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
@@ -309,9 +324,9 @@ export default class IfBlock extends Node {
 		if (dynamic) {
 			block.builders.update.addBlock(deindent`
 				var ${previous_block_index} = ${current_block_type_index};
-				${current_block_type_index} = ${select_block_type}(state);
+				${current_block_type_index} = ${select_block_type}(ctx);
 				if (${current_block_type_index} === ${previous_block_index}) {
-					${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, state);
+					${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
 				} else {
 					${changeBlock}
 				}
@@ -319,7 +334,7 @@ export default class IfBlock extends Node {
 		} else {
 			block.builders.update.addBlock(deindent`
 				var ${previous_block_index} = ${current_block_type_index};
-				${current_block_type_index} = ${select_block_type}(state);
+				${current_block_type_index} = ${select_block_type}(ctx);
 				if (${current_block_type_index} !== ${previous_block_index}) {
 					${changeBlock}
 				}
@@ -343,7 +358,7 @@ export default class IfBlock extends Node {
 		{ name, anchor, if_name }
 	) {
 		block.builders.init.addBlock(deindent`
-			var ${name} = (${branch.condition}) && ${branch.block}(#component, state);
+			var ${name} = (${branch.condition}) && ${branch.block}(#component, ctx);
 		`);
 
 		const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm';
@@ -360,9 +375,9 @@ export default class IfBlock extends Node {
 			? branch.hasIntroMethod
 				? deindent`
 					if (${name}) {
-						${name}.p(changed, state);
+						${name}.p(changed, ctx);
 					} else {
-						${name} = ${branch.block}(#component, state);
+						${name} = ${branch.block}(#component, ctx);
 						if (${name}) ${name}.c();
 					}
 
@@ -370,9 +385,9 @@ export default class IfBlock extends Node {
 				`
 				: deindent`
 					if (${name}) {
-						${name}.p(changed, state);
+						${name}.p(changed, ctx);
 					} else {
-						${name} = ${branch.block}(#component, state);
+						${name} = ${branch.block}(#component, ctx);
 						${name}.c();
 						${name}.m(${updateMountNode}, ${anchor});
 					}
@@ -380,14 +395,14 @@ export default class IfBlock extends Node {
 			: branch.hasIntroMethod
 				? deindent`
 					if (!${name}) {
-						${name} = ${branch.block}(#component, state);
+						${name} = ${branch.block}(#component, ctx);
 						${name}.c();
 					}
 					${name}.i(${updateMountNode}, ${anchor});
 				`
 				: deindent`
 					if (!${name}) {
-						${name} = ${branch.block}(#component, state);
+						${name} = ${branch.block}(#component, ctx);
 						${name}.c();
 						${name}.m(${updateMountNode}, ${anchor});
 					}
@@ -426,13 +441,11 @@ export default class IfBlock extends Node {
 		block: Block,
 		parentNode: string,
 		parentNodes: string,
-		node: Node
+		node: IfBlock
 	) {
-		block.contextualise(node.expression); // TODO remove
-
 		const branches = [
 			{
-				condition: node.metadata.snippet,
+				condition: node.expression.snippet,
 				block: node.block.name,
 				hasUpdateMethod: node.block.hasUpdateMethod,
 				hasIntroMethod: node.block.hasIntroMethod,
diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts
index 457c8538ca..d8c468224d 100644
--- a/src/generators/nodes/Text.ts
+++ b/src/generators/nodes/Text.ts
@@ -33,6 +33,11 @@ export default class Text extends Node {
 	data: string;
 	shouldSkip: boolean;
 
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+		this.data = info.data;
+	}
+
 	init(block: Block) {
 		const parentElement = this.findNearest(/(?:Element|Component)/);
 
diff --git a/src/generators/nodes/Transition.ts b/src/generators/nodes/Transition.ts
index d110b5878d..9ed9f87dab 100644
--- a/src/generators/nodes/Transition.ts
+++ b/src/generators/nodes/Transition.ts
@@ -1,7 +1,18 @@
 import Node from './shared/Node';
+import Expression from './shared/Expression';
 
 export default class Transition extends Node {
+	type: 'Transition';
 	name: string;
-	value: Node[]
-	expression: Node
+	expression: Expression;
+
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+
+		this.name = info.name;
+
+		this.expression = info.expression
+			? new Expression(compiler, this, info.expression)
+			: null;
+	}
 }
\ No newline at end of file
diff --git a/src/generators/nodes/Window.ts b/src/generators/nodes/Window.ts
index 38a38c1084..ab532a0081 100644
--- a/src/generators/nodes/Window.ts
+++ b/src/generators/nodes/Window.ts
@@ -7,7 +7,8 @@ import validCalleeObjects from '../../utils/validCalleeObjects';
 import reservedNames from '../../utils/reservedNames';
 import Node from './shared/Node';
 import Block from '../dom/Block';
-import Attribute from './Attribute';
+import Binding from './Binding';
+import EventHandler from './EventHandler';
 
 const associatedEvents = {
 	innerWidth: 'resize',
@@ -34,99 +35,107 @@ const readonly = new Set([
 
 export default class Window extends Node {
 	type: 'Window';
-	attributes: Attribute[];
+	handlers: EventHandler[];
+	bindings: Binding[];
+
+	constructor(compiler, parent, info) {
+		super(compiler, parent, info);
+
+		this.handlers = [];
+		this.bindings = [];
+
+		info.attributes.forEach(node => {
+			if (node.type === 'EventHandler') {
+				this.handlers.push(new EventHandler(compiler, this, node));
+			} else if (node.type === 'Binding') {
+				this.bindings.push(new Binding(compiler, this, node));
+			}
+		});
+	}
 
 	build(
 		block: Block,
 		parentNode: string,
 		parentNodes: string
 	) {
-		const { generator } = this;
+		const { compiler } = this;
 
 		const events = {};
 		const bindings: Record<string, string> = {};
 
-		this.attributes.forEach((attribute: Node) => {
-			if (attribute.type === 'EventHandler') {
-				// TODO verify that it's a valid callee (i.e. built-in or declared method)
-				generator.addSourcemapLocations(attribute.expression);
+		this.handlers.forEach(handler => {
+			// TODO verify that it's a valid callee (i.e. built-in or declared method)
+			compiler.addSourcemapLocations(handler.expression);
 
-				const isCustomEvent = generator.events.has(attribute.name);
+			const isCustomEvent = compiler.events.has(handler.name);
 
-				let usesState = false;
+			let usesState = handler.dependencies.size > 0;
 
-				attribute.expression.arguments.forEach((arg: Node) => {
-					block.contextualise(arg, null, true);
-					const { dependencies } = arg.metadata;
-					if (dependencies.length) usesState = true;
-				});
-
-				const flattened = flattenReference(attribute.expression.callee);
-				if (flattened.name !== 'event' && flattened.name !== 'this') {
-					// allow event.stopPropagation(), this.select() etc
-					generator.code.prependRight(
-						attribute.expression.start,
-						`${block.alias('component')}.`
-					);
-				}
+			// const flattened = flattenReference(handler.expression.callee);
+			// if (flattened.name !== 'event' && flattened.name !== 'this') {
+			// 	// allow event.stopPropagation(), this.select() etc
+			// 	compiler.code.prependRight(
+			// 		handler.expression.start,
+			// 		`${block.alias('component')}.`
+			// 	);
+			// }
 
-				const handlerName = block.getUniqueName(`onwindow${attribute.name}`);
-				const handlerBody = deindent`
-					${usesState && `var state = #component.get();`}
-					[✂${attribute.expression.start}-${attribute.expression.end}✂];
-				`;
+			const handlerName = block.getUniqueName(`onwindow${handler.name}`);
+			const handlerBody = deindent`
+				${usesState && `var ctx = #component.get();`}
+				${handler.snippet};
+			`;
 
-				if (isCustomEvent) {
-					// TODO dry this out
-					block.addVariable(handlerName);
+			if (isCustomEvent) {
+				// TODO dry this out
+				block.addVariable(handlerName);
+
+				block.builders.hydrate.addBlock(deindent`
+					${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
+						${handlerBody}
+					});
+				`);
+
+				block.builders.destroy.addLine(deindent`
+					${handlerName}.destroy();
+				`);
+			} else {
+				block.builders.init.addBlock(deindent`
+					function ${handlerName}(event) {
+						${handlerBody}
+					}
+					window.addEventListener("${handler.name}", ${handlerName});
+				`);
 
-					block.builders.hydrate.addBlock(deindent`
-						${handlerName} = %events-${attribute.name}.call(#component, window, function(event) {
-							${handlerBody}
-						});
-					`);
-
-					block.builders.destroy.addLine(deindent`
-						${handlerName}.destroy();
-					`);
-				} else {
-					block.builders.init.addBlock(deindent`
-						function ${handlerName}(event) {
-							${handlerBody}
-						}
-						window.addEventListener("${attribute.name}", ${handlerName});
-					`);
-
-					block.builders.destroy.addBlock(deindent`
-						window.removeEventListener("${attribute.name}", ${handlerName});
-					`);
-				}
+				block.builders.destroy.addBlock(deindent`
+					window.removeEventListener("${handler.name}", ${handlerName});
+				`);
 			}
+		});
 
-			if (attribute.type === 'Binding') {
-				// in dev mode, throw if read-only values are written to
-				if (readonly.has(attribute.name)) {
-					generator.readonly.add(attribute.value.name);
-				}
+		this.bindings.forEach(binding => {
+			// in dev mode, throw if read-only values are written to
+			if (readonly.has(binding.name)) {
+				compiler.readonly.add(binding.value.name);
+			}
 
-				bindings[attribute.name] = attribute.value.name;
+			bindings[binding.name] = binding.value.name;
 
-				// bind:online is a special case, we need to listen for two separate events
-				if (attribute.name === 'online') return;
+			// bind:online is a special case, we need to listen for two separate events
+			if (binding.name === 'online') return;
 
-				const associatedEvent = associatedEvents[attribute.name];
-				const property = properties[attribute.name] || attribute.name;
+			const associatedEvent = associatedEvents[binding.name];
+			const property = properties[binding.name] || binding.name;
 
-				if (!events[associatedEvent]) events[associatedEvent] = [];
-				events[associatedEvent].push(
-					`${attribute.value.name}: this.${property}`
-				);
+			if (!events[associatedEvent]) events[associatedEvent] = [];
+			events[associatedEvent].push(
+				`${binding.value.name}: this.${property}`
+			);
 
-				// add initial value
-				generator.metaBindings.push(
-					`this._state.${attribute.value.name} = window.${property};`
-				);
-			}
+			// add initial value
+			compiler.metaBindings.push(
+				`this._state.${binding.value.name} = window.${property};`
+			);
 		});
 
 		const lock = block.getUniqueName(`window_updating`);
diff --git a/src/generators/nodes/shared/Expression.ts b/src/generators/nodes/shared/Expression.ts
index 868ec45ab5..3e96677fa2 100644
--- a/src/generators/nodes/shared/Expression.ts
+++ b/src/generators/nodes/shared/Expression.ts
@@ -1,11 +1,68 @@
 import Generator from '../../Generator';
+import { walk } from 'estree-walker';
+import isReference from 'is-reference';
+import flattenReference from '../../../utils/flattenReference';
+import { createScopes } from '../../../utils/annotateWithScopes';
 
 export default class Expression {
 	compiler: Generator;
-	info: any;
+	node: any;
+	snippet: string;
 
-	constructor(compiler, info) {
+	references: Set<string>;
+	dependencies: Set<string>;
+
+	constructor(compiler, parent, info) {
 		this.compiler = compiler;
-		this.info = info;
+		this.node = info;
+
+		this.snippet = `[✂${info.start}-${info.end}✂]`;
+
+		const contextDependencies = new Map(); // TODO
+		const indexes = new Map();
+
+		const dependencies = new Set();
+
+		const { code, helpers } = compiler;
+
+		let { map, scope } = createScopes(info);
+
+		walk(info, {
+			enter(node: any, parent: any) {
+				code.addSourcemapLocation(node.start);
+				code.addSourcemapLocation(node.end);
+
+				if (map.has(node)) {
+					scope = map.get(node);
+					return;
+				}
+
+				if (isReference(node, parent)) {
+					code.prependRight(node.start, 'ctx.');
+
+					const { name } = flattenReference(node);
+					if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
+
+					if (contextDependencies.has(name)) {
+						contextDependencies.get(name).forEach(dependency => {
+							dependencies.add(dependency);
+						});
+					} else if (!indexes.has(name)) {
+						dependencies.add(name);
+					}
+
+					this.skip();
+				}
+			},
+
+			leave(node: Node, parent: Node) {
+				if (map.has(node)) scope = scope.parent;
+			}
+		});
+
+		this.dependencies = dependencies;
+
+		this.contexts = new Set(); // TODO...
+		this.indexes = new Set(); // TODO...
 	}
 }
\ No newline at end of file
diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts
index 6fb2f8cc15..a07b4fdd8b 100644
--- a/src/generators/nodes/shared/Node.ts
+++ b/src/generators/nodes/shared/Node.ts
@@ -4,8 +4,12 @@ import Block from '../../dom/Block';
 import { trimStart, trimEnd } from '../../../utils/trim';
 
 export default class Node {
-	compiler: Generator;
-	parent: Node;
+	readonly start: number;
+	readonly end: number;
+	readonly compiler: Generator;
+	readonly parent: Node;
+	readonly type: string;
+
 	prev?: Node;
 	next?: Node;
 
@@ -13,8 +17,11 @@ export default class Node {
 	var: string;
 
 	constructor(compiler: Generator, parent, info: any) {
+		this.start = info.start;
+		this.end = info.end;
 		this.compiler = compiler;
 		this.parent = parent;
+		this.type = info.type;
 	}
 
 	cannotUseInnerHTML() {
@@ -74,7 +81,7 @@ export default class Node {
 		lastChild = null;
 
 		cleaned.forEach((child: Node, i: number) => {
-			child.canUseInnerHTML = !this.generator.hydratable;
+			child.canUseInnerHTML = !this.compiler.hydratable;
 
 			child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);
 
diff --git a/src/generators/nodes/shared/Tag.ts b/src/generators/nodes/shared/Tag.ts
index b762e09311..267d93d28d 100644
--- a/src/generators/nodes/shared/Tag.ts
+++ b/src/generators/nodes/shared/Tag.ts
@@ -7,15 +7,14 @@ export default class Tag extends Node {
 
 	constructor(compiler, parent, info) {
 		super(compiler, parent, info);
-		this.expression = new Expression(compiler, info.expression);
+		this.expression = new Expression(compiler, this, info.expression);
 	}
 
 	renameThisMethod(
 		block: Block,
 		update: ((value: string) => string)
 	) {
-		const { indexes } = block.contextualise(this.expression);
-		const { dependencies, snippet } = this.metadata;
+		const { snippet, dependencies, indexes } = this.expression;
 
 		const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
 
@@ -30,16 +29,16 @@ export default class Tag extends Node {
 
 		if (shouldCache) block.addVariable(value, snippet);
 
-		if (dependencies.length || hasChangeableIndex) {
+		if (dependencies.size || hasChangeableIndex) {
 			const changedCheck = (
 				(block.hasOutroMethod ? `#outroing || ` : '') +
-				dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
+				[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
 			);
 
 			const updateCachedValue = `${value} !== (${value} = ${snippet})`;
 
 			const condition = shouldCache ?
-				(dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
+				(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
 				changedCheck;
 
 			block.builders.update.addConditional(
diff --git a/src/generators/nodes/shared/mapChildren.ts b/src/generators/nodes/shared/mapChildren.ts
index aaac0edc2d..829a7ea962 100644
--- a/src/generators/nodes/shared/mapChildren.ts
+++ b/src/generators/nodes/shared/mapChildren.ts
@@ -1,13 +1,21 @@
+import Component from '../Component';
+import EachBlock from '../EachBlock';
 import Element from '../Element';
+import IfBlock from '../IfBlock';
 import Text from '../Text';
 import MustacheTag from '../MustacheTag';
+import Window from '../Window';
 import Node from './Node';
 
 function getConstructor(type): typeof Node {
 	switch (type) {
+		case 'Component': return Component;
+		case 'EachBlock': return EachBlock;
 		case 'Element': return Element;
+		case 'IfBlock': return IfBlock;
 		case 'Text': return Text;
 		case 'MustacheTag': return MustacheTag;
+		case 'Window': return Window;
 		default: throw new Error(`Not implemented: ${type}`);
 	}
 }
diff --git a/src/generators/server-side-rendering/Block.ts b/src/generators/server-side-rendering/Block.ts
index 5b727dc828..1e85b0c1d8 100644
--- a/src/generators/server-side-rendering/Block.ts
+++ b/src/generators/server-side-rendering/Block.ts
@@ -42,7 +42,7 @@ export default class Block {
 		return new Block(Object.assign({}, this, options, { parent: this }));
 	}
 
-	contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
-		return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
-	}
+	// contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
+	// 	return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
+	// }
 }
diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts
index a0eade1664..c8e9cda42f 100644
--- a/src/generators/server-side-rendering/index.ts
+++ b/src/generators/server-side-rendering/index.ts
@@ -64,7 +64,7 @@ export default function ssr(
 		conditions: [],
 	});
 
-	trim(parsed.html.children).forEach((node: Node) => {
+	trim(generator.fragment.children).forEach((node: Node) => {
 		visit(generator, mainBlock, node);
 	});
 
@@ -93,7 +93,7 @@ export default function ssr(
 		initialState.push('{}');
 	}
 
-	initialState.push('state');
+	initialState.push('ctx');
 
 	// TODO concatenate CSS maps
 	const result = deindent`
@@ -129,15 +129,15 @@ export default function ssr(
 			};
 		}
 
-		${name}._render = function(__result, state, options) {
+		${name}._render = function(__result, ctx, options) {
 			${templateProperties.store && `options.store = %store();`}
 			__result.addComponent(${name});
 
-			state = Object.assign(${initialState.join(', ')});
+			ctx = Object.assign(${initialState.join(', ')});
 
 			${computations.map(
 				({ key, deps }) =>
-					`state.${key} = %computed-${key}(state);`
+					`ctx.${key} = %computed-${key}(ctx);`
 			)}
 
 			${generator.bindings.length &&
diff --git a/src/generators/server-side-rendering/visitors/AwaitBlock.ts b/src/generators/server-side-rendering/visitors/AwaitBlock.ts
index 1570647db5..c77bd8dbf8 100644
--- a/src/generators/server-side-rendering/visitors/AwaitBlock.ts
+++ b/src/generators/server-side-rendering/visitors/AwaitBlock.ts
@@ -8,8 +8,7 @@ export default function visitAwaitBlock(
 	block: Block,
 	node: Node
 ) {
-	block.contextualise(node.expression);
-	const { snippet } = node.metadata;
+	const { snippet } = node.expression;
 
 	// TODO should this be the generator's job? It's duplicated between
 	// here and the equivalent DOM compiler visitor
diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts
index 7ff6969983..e6a7707004 100644
--- a/src/generators/server-side-rendering/visitors/Component.ts
+++ b/src/generators/server-side-rendering/visitors/Component.ts
@@ -18,8 +18,7 @@ export default function visitComponent(
 			return escapeTemplate(escape(chunk.data));
 		}
 		if (chunk.type === 'MustacheTag') {
-			block.contextualise(chunk.expression);
-			const { snippet } = chunk.metadata;
+			const { snippet } = chunk.expression;
 			return '${__escape( ' + snippet + ')}';
 		}
 	}
diff --git a/src/generators/server-side-rendering/visitors/IfBlock.ts b/src/generators/server-side-rendering/visitors/IfBlock.ts
index 88de7f0b5a..dc5b908b33 100644
--- a/src/generators/server-side-rendering/visitors/IfBlock.ts
+++ b/src/generators/server-side-rendering/visitors/IfBlock.ts
@@ -8,8 +8,7 @@ export default function visitIfBlock(
 	block: Block,
 	node: Node
 ) {
-	block.contextualise(node.expression);
-	const { snippet } = node.metadata;
+	const { snippet } = node.expression;
 
 	generator.append('${ ' + snippet + ' ? `');
 
diff --git a/src/generators/server-side-rendering/visitors/MustacheTag.ts b/src/generators/server-side-rendering/visitors/MustacheTag.ts
index 795e78dd33..713493ac79 100644
--- a/src/generators/server-side-rendering/visitors/MustacheTag.ts
+++ b/src/generators/server-side-rendering/visitors/MustacheTag.ts
@@ -7,8 +7,7 @@ export default function visitMustacheTag(
 	block: Block,
 	node: Node
 ) {
-	block.contextualise(node.expression);
-	const { snippet } = node.metadata;
+	const { snippet } = node.expression;
 
 	generator.append(
 		node.parent &&
diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts
index cf0267c240..2eb8b3d068 100644
--- a/src/parse/state/tag.ts
+++ b/src/parse/state/tag.ts
@@ -113,7 +113,7 @@ export default function tag(parser: Parser) {
 
 	const type = metaTags.has(name)
 		? metaTags.get(name)
-		: 'Element'; // TODO in v2, capitalised name means 'Component'
+		: /[A-Z]/.test(name[0]) ? 'Component' : 'Element';
 
 	const element: Node = {
 		start,
diff --git a/src/utils/addToSet.ts b/src/utils/addToSet.ts
new file mode 100644
index 0000000000..5197e96972
--- /dev/null
+++ b/src/utils/addToSet.ts
@@ -0,0 +1,5 @@
+export default function addToSet(a: Set<any>, b: Set<any>) {
+	b.forEach(item => {
+		a.add(item);
+	});
+}
\ No newline at end of file
diff --git a/src/utils/annotateWithScopes.ts b/src/utils/annotateWithScopes.ts
index 41e734b6e9..408de776b2 100644
--- a/src/utils/annotateWithScopes.ts
+++ b/src/utils/annotateWithScopes.ts
@@ -2,6 +2,54 @@ import { walk } from 'estree-walker';
 import isReference from 'is-reference';
 import { Node } from '../interfaces';
 
+export function createScopes(expression: Node) {
+	const map = new WeakMap();
+
+	const globals = new Set();
+	let scope = new Scope(null, false);
+
+	walk(expression, {
+		enter(node: Node, parent: Node) {
+			if (/Function/.test(node.type)) {
+				if (node.type === 'FunctionDeclaration') {
+					scope.declarations.add(node.id.name);
+				} else {
+					scope = new Scope(scope, false);
+					map.set(node, scope);
+					if (node.id) scope.declarations.add(node.id.name);
+				}
+
+				node.params.forEach((param: Node) => {
+					extractNames(param).forEach(name => {
+						scope.declarations.add(name);
+					});
+				});
+			} else if (/For(?:In|Of)Statement/.test(node.type)) {
+				scope = new Scope(scope, true);
+				map.set(node, scope);
+			} else if (node.type === 'BlockStatement') {
+				scope = new Scope(scope, true);
+				map.set(node, scope);
+			} else if (/(Function|Class|Variable)Declaration/.test(node.type)) {
+				scope.addDeclaration(node);
+			} else if (isReference(node, parent)) {
+				if (!scope.has(node.name)) {
+					globals.add(node.name);
+				}
+			}
+		},
+
+		leave(node: Node) {
+			if (map.has(node)) {
+				scope = scope.parent;
+			}
+		},
+	});
+
+	return { map, scope, globals };
+}
+
+// TODO remove this in favour of weakmap version
 export default function annotateWithScopes(expression: Node) {
 	const globals = new Set();
 	let scope = new Scope(null, false);
diff --git a/src/utils/createDebuggingComment.ts b/src/utils/createDebuggingComment.ts
index 1eadfd1c93..61dc2ddba0 100644
--- a/src/utils/createDebuggingComment.ts
+++ b/src/utils/createDebuggingComment.ts
@@ -2,20 +2,21 @@ import { DomGenerator } from '../generators/dom/index';
 import { Node } from '../interfaces';
 
 export default function createDebuggingComment(node: Node, generator: DomGenerator) {
-	const { locate, source } = generator;
+	return `TODO ${node.start}-${node.end}`;
+	// const { locate, source } = generator;
 
-	let c = node.start;
-	if (node.type === 'ElseBlock') {
-		while (source[c - 1] !== '{') c -= 1;
-		while (source[c - 1] === '{') c -= 1;
-	}
+	// let c = node.start;
+	// if (node.type === 'ElseBlock') {
+	// 	while (source[c - 1] !== '{') c -= 1;
+	// 	while (source[c - 1] === '{') c -= 1;
+	// }
 
-	let d = node.expression ? node.expression.end : c;
-	while (source[d] !== '}') d += 1;
-	while (source[d] === '}') d += 1;
+	// let d = node.expression ? node.expression.end : c;
+	// while (source[d] !== '}') d += 1;
+	// while (source[d] === '}') d += 1;
 
-	const start = locate(c);
-	const loc = `(${start.line + 1}:${start.column})`;
+	// const start = locate(c);
+	// const loc = `(${start.line + 1}:${start.column})`;
 
-	return `${loc} ${source.slice(c, d)}`.replace(/\s/g, ' ');
+	// return `${loc} ${source.slice(c, d)}`.replace(/\s/g, ' ');
 }
diff --git a/test/runtime/index.js b/test/runtime/index.js
index 199a9a0c9b..8f37b7bb84 100644
--- a/test/runtime/index.js
+++ b/test/runtime/index.js
@@ -25,7 +25,7 @@ function getName(filename) {
 	return base[0].toUpperCase() + base.slice(1);
 }
 
-describe("runtime", () => {
+describe.only("runtime", () => {
 	before(() => {
 		svelte = loadSvelte(false);
 		svelte$ = loadSvelte(true);
diff --git a/test/runtime/samples/attribute-dynamic/_config.js b/test/runtime/samples/attribute-dynamic/_config.js
index f6afbeca10..9b33022e62 100644
--- a/test/runtime/samples/attribute-dynamic/_config.js
+++ b/test/runtime/samples/attribute-dynamic/_config.js
@@ -1,5 +1,4 @@
 export default {
-	solo: true,
 	html: `<div style="color: red;">red</div>`,
 
 	test ( assert, component, target ) {
diff --git a/test/runtime/samples/binding-input-text/_config.js b/test/runtime/samples/binding-input-text/_config.js
index ccbe234d12..2c54fc8c32 100644
--- a/test/runtime/samples/binding-input-text/_config.js
+++ b/test/runtime/samples/binding-input-text/_config.js
@@ -1,21 +1,32 @@
 export default {
 	data: {
-		name: 'world'
+		name: 'world',
 	},
-	html: `<input>\n<p>hello world</p>`,
-	test ( assert, component, target, window ) {
-		const input = target.querySelector( 'input' );
-		assert.equal( input.value, 'world' );
 
-		const event = new window.Event( 'input' );
+	html: `
+		<input>
+		<p>hello world</p>
+	`,
+
+	test(assert, component, target, window) {
+		const input = target.querySelector('input');
+		assert.equal(input.value, 'world');
+
+		const event = new window.Event('input');
 
 		input.value = 'everybody';
-		input.dispatchEvent( event );
+		input.dispatchEvent(event);
 
-		assert.equal( target.innerHTML, `<input>\n<p>hello everybody</p>` );
+		assert.htmlEqual(target.innerHTML, `
+			<input>
+			<p>hello everybody</p>
+		`);
 
 		component.set({ name: 'goodbye' });
-		assert.equal( input.value, 'goodbye' );
-		assert.equal( target.innerHTML, `<input>\n<p>hello goodbye</p>` );
-	}
+		assert.equal(input.value, 'goodbye');
+		assert.htmlEqual(target.innerHTML, `
+			<input>
+			<p>hello goodbye</p>
+		`);
+	},
 };
diff --git a/test/runtime/samples/trait-function/main.html b/test/runtime/samples/trait-function/main.html
deleted file mode 100644
index cde902caad..0000000000
--- a/test/runtime/samples/trait-function/main.html
+++ /dev/null
@@ -1,46 +0,0 @@
-<button use:tooltip="t(actionTransKey)">action</button>
-
-<script>
-	const translations = {
-		perform_action: 'Perform an Action'
-	};
-
-	function t(key) {
-		return translations[key] || `{{${key}}}`;
-	}
-
-	export default {
-		data() {
-			return { t, actionTransKey: 'perform_action' };
-		},
-
-		actions: {
-			tooltip(node, text) {
-				let tooltip = null;
-
-				function onMouseEnter() {
-					tooltip = document.createElement('div');
-					tooltip.classList.add('tooltip');
-					tooltip.textContent = text;
-					node.parentNode.appendChild(tooltip);
-				}
-
-				function onMouseLeave() {
-					if (!tooltip) return;
-					tooltip.remove();
-					tooltip = null;
-				}
-
-				node.addEventListener('mouseenter', onMouseEnter);
-				node.addEventListener('mouseleave', onMouseLeave);
-				
-				return {
-					destroy() {
-						node.removeEventListener('mouseenter', onMouseEnter);
-						node.removeEventListener('mouseleave', onMouseLeave);
-					}
-				}
-			}
-		}
-	}
-</script>
\ No newline at end of file