Change of behaviour in Elixir 1.18 macros: (ArgumentError) tried to unquote invalid AST

In that case I guess you can compile the regular expression in compile time, then inspect it to get a string with sigil and finally convert a string to the AST.

Here is a complete script I have prepared:

Mix.install([:ecto])

defmodule MyLib.Schema do
  @doc """
  Generates the changeset function.

  The `name` is a changeset function name. Default to `:changeset`.
  The `properties` is an `Elixir` map with `atom` keys.
  The `func` is an optional funtion that could be used by the developer to add other validations. 
  """
  defmacro changeset(name \\ :changeset, properties, func \\ nil) do
    quote bind_quoted: [func: func, module: __MODULE__, name: name, properties: properties] do
      pipe =
        properties
        |> module.from_properties(__MODULE__)
        |> module.pipe_func(func)

      # pipe
      # |> Macro.to_string()
      # |> Code.format_string!()
      # =>
      # struct
      # |> cast(params, [:four_digit_code])
      # |> validate_format(:four_digit_code, ~r/[0-9]{4}/)

      struct = Macro.var(:struct, __MODULE__)
      params = Macro.var(:params, __MODULE__)

      def unquote(name)(unquote(struct), unquote(params)) do
        unquote(pipe)
      end
    end
  end

  @doc false
  def from_properties(properties, module) do
    fields = Map.keys(properties)
    struct = Macro.var(:struct, module)
    params = Macro.var(:params, module)

    cast =
      quote do
        cast(unquote(params), unquote(fields))
      end

    pipe = ast_pipe(struct, cast)
    Enum.reduce(properties, pipe, &from_field_properties/2)
  end

  @supported_validators ~w[pattern]a

  defp from_field_properties({field, properties}, acc) do
    properties
    |> Map.take(@supported_validators)
    |> Enum.reduce(acc, fn {key, value}, acc ->
      ast_pipe(acc, validator(field, key, value))
    end)
  end

  defp validator(field, :pattern, value) do
    quote do
      validate_format(unquote(field), unquote(quoted_regex_sigil(value)))
    end
  end

  defp quoted_regex_sigil(source) do
    source
    # creates regular expression from source
    |> Regex.compile!()
    # inspect returns a string with a sigil
    |> inspect()
    # escaped AST form
    |> Code.string_to_quoted!()
  end

  def pipe_func(left, nil), do: left

  def pipe_func(left, func) do
    ast_pipe(
      left,
      quote do
        then(unquote(func))
      end
    )
  end

  defp ast_pipe(left, right) do
    quote do
      unquote(left) |> unquote(right)
    end
  end
end

defmodule MyApp.Schema do
  use Ecto.Schema

  import Ecto.Changeset
  import MyLib.Schema

  embedded_schema do
    field(:four_digit_code, :string)
  end

  changeset(%{four_digit_code: %{pattern: "[0-9]{4}"}})
end

defmodule Example do
  def sample do
    MyApp.Schema.changeset(%MyApp.Schema{}, %{four_digit_code: "0007"})
    # => %Ecto.Changeset{valid?: true}
  end
end

You should be able to easily adapt the example code to your needs.