function create_scopes(ast, root, allow_reactive_declarations, parent) {
	/** @typedef {{ scope: Scope }} State */

	/**
	 * A map of node->associated scope. A node appearing in this map does not necessarily mean that it created a scope
	 * @type {Map<AST.SvelteNode, Scope>}
	 */
	const scopes = new Map();
	const scope = new Scope(root, parent, false);
	scopes.set(ast, scope);

	/** @type {State} */
	const state = { scope };

	/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
	const references = [];

	/** @type {[Scope, Pattern | MemberExpression][]} */
	const updates = [];

	/**
	 * An array of reactive declarations, i.e. the `a` in `$: a = b * 2`
	 * @type {Identifier[]}
	 */
	const possible_implicit_declarations = [];

	/**
	 * @param {Scope} scope
	 * @param {Pattern[]} params
	 */
	function add_params(scope, params) {
		for (const param of params) {
			for (const node of extract_identifiers(param)) {
				scope.declare(node, 'normal', param.type === 'RestElement' ? 'rest_param' : 'param');
			}
		}
	}

	/**
	 * @type {Visitor<Node, State, AST.SvelteNode>}
	 */
	const create_block_scope = (node, { state, next }) => {
		const scope = state.scope.child(true);
		scopes.set(node, scope);

		next({ scope });
	};

	/**
	 * @type {Visitor<AST.ElementLike, State, AST.SvelteNode>}
	 */
	const SvelteFragment = (node, { state, next }) => {
		const scope = state.scope.child();
		scopes.set(node, scope);
		next({ scope });
	};

	/**
	 * @type {Visitor<AST.Component | AST.SvelteComponent | AST.SvelteSelf, State, AST.SvelteNode>}
	 */
	const Component = (node, context) => {
		node.metadata.scopes = {
			default: context.state.scope.child()
		};

		if (node.type === 'SvelteComponent') {
			context.visit(node.expression);
		}

		const default_state = determine_slot(node)
			? context.state
			: { scope: node.metadata.scopes.default };

		for (const attribute of node.attributes) {
			if (attribute.type === 'LetDirective') {
				context.visit(attribute, default_state);
			} else {
				context.visit(attribute);
			}
		}

		for (const child of node.fragment.nodes) {
			let state = default_state;

			const slot_name = determine_slot(child);

			if (slot_name !== null) {
				node.metadata.scopes[slot_name] = context.state.scope.child();

				state = {
					scope: node.metadata.scopes[slot_name]
				};
			}

			context.visit(child, state);
		}
	};

	/**
	 * @type {Visitor<AST.AnimateDirective | AST.TransitionDirective | AST.UseDirective, State, AST.SvelteNode>}
	 */
	const SvelteDirective = (node, { state, path, visit }) => {
		state.scope.reference(b.id(node.name.split('.')[0]), path);

		if (node.expression) {
			visit(node.expression);
		}
	};

	walk(ast, state, {
		// references
		Identifier(node, { path, state }) {
			const parent = path.at(-1);
			if (
				parent &&
				is_reference(node, /** @type {Node} */ (parent)) &&
				// TSTypeAnnotation, TSInterfaceDeclaration etc - these are normally already filtered out,
				// but for the migration they aren't, so we need to filter them out here
				// TODO -> once migration script is gone we can remove this check
				!parent.type.startsWith('TS')
			) {
				references.push([state.scope, { node, path: path.slice() }]);
			}
		},
		LabeledStatement(node, { path, next }) {
			if (path.length > 1 || !allow_reactive_declarations) return next();
			if (node.label.name !== '$') return next();

			// create a scope for the $: block
			const scope = state.scope.child();
			scopes.set(node, scope);

			if (
				node.body.type === 'ExpressionStatement' &&
				node.body.expression.type === 'AssignmentExpression'
			) {
				for (const id of extract_identifiers(node.body.expression.left)) {
					if (!id.name.startsWith('$')) {
						possible_implicit_declarations.push(id);
					}
				}
			}

			next({ scope });
		},

		SvelteFragment,
		SlotElement: SvelteFragment,
		SvelteElement: SvelteFragment,
		RegularElement: SvelteFragment,

		LetDirective(node, context) {
			const scope = context.state.scope;

			/** @type {Binding[]} */
			const bindings = [];
			scope.declarators.set(node, bindings);

			if (node.expression) {
				for (const id of extract_identifiers_from_destructuring(node.expression)) {
					const binding = scope.declare(id, 'template', 'const');
					scope.reference(id, [context.path[context.path.length - 1], node]);
					bindings.push(binding);
				}
			} else {
				/** @type {Identifier} */
				const id = {
					name: node.name,
					type: 'Identifier',
					start: node.start,
					end: node.end
				};
				const binding = scope.declare(id, 'template', 'const');
				scope.reference(id, [context.path[context.path.length - 1], node]);
				bindings.push(binding);
			}
		},

		Component: (node, context) => {
			context.state.scope.reference(b.id(node.name), context.path);
			Component(node, context);
		},
		SvelteSelf: Component,
		SvelteComponent: Component,

		// updates
		AssignmentExpression(node, { state, next }) {
			updates.push([state.scope, node.left]);
			next();
		},

		UpdateExpression(node, { state, next }) {
			updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]);
			next();
		},

		ImportDeclaration(node, { state }) {
			for (const specifier of node.specifiers) {
				state.scope.declare(specifier.local, 'normal', 'import', node);
			}
		},

		FunctionExpression(node, { state, next }) {
			const scope = state.scope.child();
			scopes.set(node, scope);

			if (node.id) scope.declare(node.id, 'normal', 'function');

			add_params(scope, node.params);
			next({ scope });
		},

		FunctionDeclaration(node, { state, next }) {
			if (node.id) state.scope.declare(node.id, 'normal', 'function', node);

			const scope = state.scope.child();
			scopes.set(node, scope);

			add_params(scope, node.params);
			next({ scope });
		},

		ArrowFunctionExpression(node, { state, next }) {
			const scope = state.scope.child();
			scopes.set(node, scope);

			add_params(scope, node.params);
			next({ scope });
		},

		ForStatement: create_block_scope,
		ForInStatement: create_block_scope,
		ForOfStatement: create_block_scope,
		SwitchStatement: create_block_scope,
		BlockStatement(node, context) {
			const parent = context.path.at(-1);
			if (
				parent?.type === 'FunctionDeclaration' ||
				parent?.type === 'FunctionExpression' ||
				parent?.type === 'ArrowFunctionExpression'
			) {
				// We already created a new scope for the function
				context.next();
			} else {
				create_block_scope(node, context);
			}
		},

		ClassDeclaration(node, { state, next }) {
			if (node.id) state.scope.declare(node.id, 'normal', 'let', node);
			next();
		},

		VariableDeclaration(node, { state, path, next }) {
			const is_parent_const_tag = path.at(-1)?.type === 'ConstTag';
			for (const declarator of node.declarations) {
				/** @type {Binding[]} */
				const bindings = [];

				state.scope.declarators.set(declarator, bindings);

				for (const id of extract_identifiers(declarator.id)) {
					const binding = state.scope.declare(
						id,
						is_parent_const_tag ? 'template' : 'normal',
						node.kind,
						declarator.init
					);
					bindings.push(binding);
				}
			}

			next();
		},

		CatchClause(node, { state, next }) {
			if (node.param) {
				const scope = state.scope.child(true);
				scopes.set(node, scope);

				for (const id of extract_identifiers(node.param)) {
					scope.declare(id, 'normal', 'let');
				}

				next({ scope });
			} else {
				next();
			}
		},

		EachBlock(node, { state, visit }) {
			visit(node.expression);

			// context and children are a new scope
			const scope = state.scope.child();
			scopes.set(node, scope);

			if (node.context) {
				// declarations
				for (const id of extract_identifiers(node.context)) {
					const binding = scope.declare(id, 'each', 'const');

					let inside_rest = false;
					let is_rest_id = false;
					walk(node.context, null, {
						Identifier(node) {
							if (inside_rest && node === id) {
								is_rest_id = true;
							}
						},
						RestElement(_, { next }) {
							const prev = inside_rest;
							inside_rest = true;
							next();
							inside_rest = prev;
						}
					});

					binding.metadata = { inside_rest: is_rest_id };
				}

				// Visit to pick up references from default initializers
				visit(node.context, { scope });
			}

			if (node.index) {
				const is_keyed =
					node.key &&
					(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index);
				scope.declare(b.id(node.index), is_keyed ? 'template' : 'static', 'const', node);
			}
			if (node.key) visit(node.key, { scope });

			// children
			for (const child of node.body.nodes) {
				visit(child, { scope });
			}
			if (node.fallback) visit(node.fallback, { scope });

			node.metadata = {
				expression: create_expression_metadata(),
				keyed: false,
				contains_group_binding: false,
				index: scope.root.unique('$$index'),
				declarations: scope.declarations,
				is_controlled: false,
				// filled in during analysis
				transitive_deps: new Set()
			};
		},

		AwaitBlock(node, context) {
			context.visit(node.expression);

			if (node.pending) {
				context.visit(node.pending);
			}

			if (node.then) {
				context.visit(node.then);
				if (node.value) {
					const then_scope = /** @type {Scope} */ (scopes.get(node.then));
					const value_scope = context.state.scope.child();
					scopes.set(node.value, value_scope);
					context.visit(node.value, { scope: value_scope });
					for (const id of extract_identifiers(node.value)) {
						then_scope.declare(id, 'template', 'const');
						value_scope.declare(id, 'normal', 'const');
					}
				}
			}

			if (node.catch) {
				context.visit(node.catch);
				if (node.error) {
					const catch_scope = /** @type {Scope} */ (scopes.get(node.catch));
					const error_scope = context.state.scope.child();
					scopes.set(node.error, error_scope);
					context.visit(node.error, { scope: error_scope });
					for (const id of extract_identifiers(node.error)) {
						catch_scope.declare(id, 'template', 'const');
						error_scope.declare(id, 'normal', 'const');
					}
				}
			}
		},

		SnippetBlock(node, context) {
			const state = context.state;
			let scope = state.scope;

			scope.declare(node.expression, 'normal', 'function', node);

			const child_scope = state.scope.child();
			scopes.set(node, child_scope);

			for (const param of node.parameters) {
				for (const id of extract_identifiers(param)) {
					child_scope.declare(id, 'snippet', 'let');
				}
			}

			context.next({ scope: child_scope });
		},

		Fragment: (node, context) => {
			const scope = context.state.scope.child(node.metadata.transparent);
			scopes.set(node, scope);
			context.next({ scope });
		},

		BindDirective(node, context) {
			updates.push([
				context.state.scope,
				/** @type {Identifier | MemberExpression} */ (node.expression)
			]);
			context.next();
		},

		TransitionDirective: SvelteDirective,
		AnimateDirective: SvelteDirective,
		UseDirective: SvelteDirective,
		// using it's own function instead of `SvelteDirective` because
		// StyleDirective doesn't have expressions and are generally already
		// handled by `Identifier`. This is the special case for the shorthand
		// eg <button style:height /> where the variable has the same name of
		// the css property
		StyleDirective(node, { path, state, next }) {
			if (node.value === true) {
				state.scope.reference(b.id(node.name), path.concat(node));
			}
			next();
		}

		// TODO others
	});

	for (const id of possible_implicit_declarations) {
		const binding = scope.get(id.name);
		if (binding) continue; // TODO can also be legacy_reactive if declared outside of reactive statement

		scope.declare(id, 'legacy_reactive', 'let');
	}

	// we do this after the fact, so that we don't need to worry
	// about encountering references before their declarations
	for (const [scope, { node, path }] of references) {
		scope.reference(node, path);
	}

	for (const [scope, node] of updates) {
		for (const expression of unwrap_pattern(node)) {
			const left = object(expression);
			const binding = left && scope.get(left.name);

			if (binding !== null && left !== binding.node) {
				if (left === expression) {
					binding.reassigned = true;
				} else {
					binding.mutated = true;
				}
			}
		}
	}

	return {
		scope,
		scopes
	};
}
@param {AST.SvelteNode} ast
@param {ScopeRoot} root
@param {boolean} allow_reactive_declarations
@param {Scope | null} parent