function build_template_chunk(
	values,
	visit,
	state,
	memoize = (value, metadata) =>
		metadata.has_call ? get_expression_id(state.expressions, value) : value
) {
	/** @type {Expression[]} */
	const expressions = [];

	let quasi = b.quasi('');
	const quasis = [quasi];

	let has_state = false;

	for (let i = 0; i < values.length; i++) {
		const node = values[i];

		if (node.type === 'Text') {
			quasi.value.cooked += node.data;
		} else if (node.expression.type === 'Literal') {
			if (node.expression.value != null) {
				quasi.value.cooked += node.expression.value + '';
			}
		} else if (
			node.expression.type !== 'Identifier' ||
			node.expression.name !== 'undefined' ||
			state.scope.get('undefined')
		) {
			let value = memoize(
				/** @type {Expression} */ (visit(node.expression, state)),
				node.metadata.expression
			);

			const evaluated = state.scope.evaluate(value);

			has_state ||= node.metadata.expression.has_state && !evaluated.is_known;

			if (values.length === 1) {
				// If we have a single expression, then pass that in directly to possibly avoid doing
				// extra work in the template_effect (instead we do the work in set_text).
				if (evaluated.is_known) {
					value = b.literal((evaluated.value ?? '') + '');
				}

				return { value, has_state };
			}

			if (
				value.type === 'LogicalExpression' &&
				value.right.type === 'Literal' &&
				(value.operator === '??' || value.operator === '||')
			) {
				// `foo ?? null` -=> `foo ?? ''`
				// otherwise leave the expression untouched
				if (value.right.value === null) {
					value = { ...value, right: b.literal('') };
				}
			}

			if (evaluated.is_known) {
				quasi.value.cooked += (evaluated.value ?? '') + '';
			} else {
				if (!evaluated.is_defined) {
					// add `?? ''` where necessary
					value = b.logical('??', value, b.literal(''));
				}

				expressions.push(value);

				quasi = b.quasi('', i + 1 === values.length);
				quasis.push(quasi);
			}
		}
	}

	for (const quasi of quasis) {
		quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
	}

	const value =
		expressions.length > 0
			? b.template(quasis, expressions)
			: b.literal(/** @type {string} */ (quasi.value.cooked));

	return { value, has_state };
}
@param {Array<AST.Text | AST.ExpressionTag>} values
@param {(node: AST.SvelteNode, state: any) => any} visit
@param {ComponentClientTransformState} state
@param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
@returns {{ value: Expression, has_state: boolean }}