function visit(root, visitors, state) {
	const { ctx } = this;
	visitingCtx = ctx;

	const afters = [];

	// Check instanceof since that's fastest, but also account for POJO nodes.
	Node = root.constructor;

	/** @type {Scope} */
	let scope;

	function enter(node, ancestors, seededPath) {
		const path = seededPath || new ctx.Path(node, ancestors.slice());
		if (node === root) {
			scope = new Scope(path, null);
		}
		ancestors.push(node);

		let prevScope = scope;
		if (types.isFunctionDeclaration(node)) {
			scope = new Scope(path, scope);

			for (let i = 0; i < node.params.length; i++) {
				const param = node.params[i];
				if (types.isIdentifier(param)) {
					const paramPath = path.get(`params.${i}`);
					scope.bindings[param.name] = new Binding(paramPath);
				} else if (types.isObjectPattern(param)) {
					for (let j = 0; j < param.properties.length; j++) {
						const prop = param.properties[j];
						if (types.isIdentifier(prop.value)) {
							const propPath = path.get(`params.${i}`);
							scope.bindings[prop.value.name] = new Binding(propPath);
						} else if (types.isAssignmentPattern(prop.value)) {
							const propPath = path.get(`params.${i}`);
							scope.bindings[prop.value.left.name] = new Binding(propPath);
						}
					}
				} else if (types.isArrayPattern(param)) {
					for (let j = 0; j < param.elements.length; j++) {
						const el = param.elements[j];
						if (types.isIdentifier(el)) {
							const elPath = path.get(`params.${i}`);
							scope.bindings[el.name] = new Binding(elPath);
						} else if (types.isAssignmentPattern(el)) {
							const propPath = path.get(`params.${i}`);
							scope.bindings[el.left.name] = new Binding(propPath);
						}
					}
				}
			}
		} else if (types.isBlockStatement(node)) {
			scope = new Scope(path, scope);
		} else if (types.isVariableDeclarator(node)) {
			if (types.isIdentifier(node.id)) {
				const name = node.id.name;
				scope.bindings[name] = new Binding(path);
			} else if (types.isArrayPattern(node.id)) {
				// Example: `const [a, b] = foo();`
				node.id.elements.forEach(item => {
					if (!item) return;
					if (types.isIdentifier(item)) {
						const name = item.name;
						scope.bindings[name] = new Binding(path);
					}
				});
			}
		} else if (
			types.isImportSpecifier(node) ||
			types.isImportDefaultSpecifier(node) ||
			types.isImportNamespaceSpecifier(node)
		) {
			const name = node.local.name;
			scope.bindings[name] = new Binding(path);
		} else if (types.isProgram(node)) {
			scope = new Scope(path, null);
		}
		path._scope = scope;

		if (path.shouldStop) {
			return false;
		}

		if (path.shouldSkip) {
			return;
		}

		// Babel has dedicated literal nodes like `NumericLiteral`
		if (node.type === 'Literal' && !(node.type in visitors)) {
			if (typeof node.value === 'number') {
				node.type = 'NumericLiteral';
			} else if (typeof node.value === 'string') {
				node.type = 'StringLiteral';
			} else if (typeof node.value === 'boolean') {
				node.type = 'BooleanLiteral';
			} else if (node.value === null) {
				node.type = 'NullLiteral';
			}
		}

		if (node.type in visitors) {
			let visitor = visitors[node.type];
			if (typeof visitor === 'object') {
				if (visitor.exit) {
					afters.push([visitor.exit, node, state, ancestors]);
				}
				if (visitor.enter) {
					visitor.enter(path, state);
				}
			} else {
				visitor(path, state);
			}
			if (ctx.queue.has(path)) {
				// node was requeued, skip (but don't stop)
				ancestors.pop();
				return;
			}
			if (path.shouldStop) {
				ancestors.pop();
				return false;
			}

			scope = prevScope;
		}

		outer: for (let i in node) {
			const v = node[i];
			if (isNode(v)) {
				if (enter(v, ancestors) === false) break;
			} else if (Array.isArray(v)) {
				for (const child of v) {
					if (isNode(child)) {
						if (enter(child, ancestors) === false) break outer;
					}
				}
			}
		}
		ancestors.pop();
	}

	enter(root, []);

	const queue = ctx.queue.values();
	let item;
	while ((item = queue.next()) && !item.done) {
		const next = item.value;
		ctx.queue.delete(next);
		enter(next.node, next.ancestors.slice(), next);
	}

	let after;
	while ((after = afters.pop())) {
		const [visitor, node, state, ancestors] = after;
		const path = new ctx.Path(node, ancestors);
		visitor(path, state);
	}
}
@param {Node} root
@param {Record<string, Visitor>} visitors
@param {object} state
@this {{ ctx: ReturnType<typeof createContext> }}