defmodule Circe do @moduledoc """ Documentation for `Circe`. """ defmacrop spliced(pat) do quote do {:"::", _, [{{:., _, [Kernel, :to_string]}, _, [[spliced: unquote(pat)]]}, _]} end end defmacrop singleton(pat) do quote do {:"::", _, [{{:., _, [Kernel, :to_string]}, _, [unquote(pat)]}, _]} end end @doc false def extract_splicing(var) do [{:"$Circe.extract_splicing", [], [var]}] end @doc false def extract(var) do {:"$Circe.extract", [], [var]} end @doc false def process_input(ast, opts) do ast = case Keyword.get(opts, :unwrap_list, false) do true -> [ast] = ast ; ast false -> ast end {prepared_ast, metas} = prepare_ast(ast) #|> IO.inspect(label: "prepare_ast") var_by_escaped = Map.new(metas, fn meta -> {Macro.escape(meta.ast), meta.var} end) #|> IO.inspect(label: "var_by_escaped") match_ast = replace_escaped(Macro.escape(prepared_ast), var_by_escaped) #|> IO.inspect(label: "replace_escaped") {match_ast, metas} end @doc false def prepare_ast([{:"$Circe.extract_splicing", _, [var]}] = ast) do {ast, [%{var: var, ast: ast}]} end def prepare_ast({:"$Circe.extract", _, [var]} = ast) do {ast, [%{var: var, ast: ast}]} end def prepare_ast({left, _meta, right}) do {ast_l, metas_l} = prepare_ast(left) {ast_r, metas_r} = prepare_ast(right) {{ast_l, :"$to_ignore", ast_r}, metas_l ++ metas_r} end def prepare_ast(xs) when is_list(xs) do Enum.unzip(Enum.map(xs, &prepare_ast/1)) |> case do {ast, metass} -> {ast, Enum.concat(metass)} end end # do block def prepare_ast({:do, x}) do {ast, metas} = prepare_ast(x) {{:do, ast}, metas} end def prepare_ast(x) when is_atom(x) do {x, []} end @doc false def replace_escaped(escaped_ast, m) do case Map.fetch(m, escaped_ast) do {:ok, ast} -> ast :error -> case escaped_ast do {left, meta, right} -> left = replace_escaped(left, m) right = replace_escaped(right, m) {left, meta, right} xs when is_list(xs) -> Enum.map(xs, fn x -> replace_escaped(x, m) end) {:do, x} -> # do block {:do, replace_escaped(x, m)} :"$to_ignore" -> Macro.var(:_, nil) x -> x end end end @doc ~S""" Handles the sigil `~m` to match on code's ast. This is meant as a symmetric tool to `quote/2`. The sigil should be used within a matching context: - `case/2` clause - function head (`def/2` and `fn/1`) - `=/2` left hand side - `match?/2`'s first argument You can directly match code: iex> import Circe iex> ast = quote do {:banana} end {:{}, [], [:banana]} iex> match?(~m/{:banana}/, ast) true You can ignore parts of the code by combining interpolation and an underscore: iex> import Circe iex> ast = quote do {:test_atom_pls_ignore} end {:{}, [], [:test_atom_pls_ignore]} iex> match?(~m/{#{_}}/, ast) true Instead of ignoring parts of the code, you can bind it to a variable: iex> import Circe iex> ~m/{#{banana}}/ = quote do {:split} end {:{}, [], [:split]} iex> banana :split Sometime the ast that would correspond to the code you wrote is wrapped in a list, (typically concerning the `->` operator), you can unwrap it with the modifier `w`: iex> import Circe iex> ast = quote do case 4 do 1 -> :one ; 2 -> :two ; 3 -> :three ; _ -> :too_big end end iex> ~m/case #{_} do #{clauses} end/ = ast iex> Enum.any?(clauses, fn ~m/(#{_} -> :too_big)/ -> true ; _ -> false end) false iex> Enum.any?(clauses, fn ~m/(#{_} -> :too_big)/w -> true ; _ -> false end) true The equivalent to `unquote_splicing/3` is `#{[spliced: _]}`: iex> import Circe iex> ~m/{#{[spliced: desserts]}}/ = quote do {:banana, :split, :peach, :melba} end {:{}, [], [:banana, :split, :peach, :melba]} iex> desserts [:banana, :split, :peach, :melba] """ defmacro sigil_m({:<<>>, _, term}, modifiers) do #_ = IO.inspect(modifiers, label: "#{__MODULE__} -- modifiers") #_ = IO.inspect(term, label: "#{__MODULE__} -- term") opts = Enum.flat_map(modifiers, fn ?w -> [unwrap_list: true] _ -> [] end) fallback_clause = quote do _ -> :no_match end {iodata, {_n, match_ast}} = Enum.map_reduce(term, {0, fallback_clause}, fn spliced(pat), {n, xs} -> tmp_name = :"circe_match_#{n}" match_ast = quote do [{unquote(tmp_name), _, _}] -> {:ok, unquote(Macro.escape(extract_splicing(pat)))} end {to_string(tmp_name), {n + 1, match_ast ++ xs}} singleton(pat), {n, xs} -> tmp_name = :"circe_match_#{n}" match_ast = quote do {unquote(tmp_name), _, ast} -> #IO.puts("MATCHED TUPLE") disambiguate = fn ^ast -> unquote(Macro.escape(extract(pat))) new_ast -> {unquote(Macro.escape(extract(pat))), [], new_ast} end {:ambiguous, ast, disambiguate} unquote(tmp_name) -> {:ok, unquote(Macro.escape(extract(pat)))} end {to_string(tmp_name), {n + 1, match_ast ++ xs}} x, acc -> {x, acc} end) {matcher, _} = Code.eval_quoted({:fn, [], match_ast}) ast = Code.string_to_quoted!(IO.iodata_to_binary(iodata), file: __CALLER__.file, line: __CALLER__.line) #|> IO.inspect(label: "string_to_quoted result --") preprocess(ast, matcher) #|> IO.inspect(label: "preprocessed ast") #|> case do x -> IO.puts("preprocessed -- #{Macro.to_string(x)}") ; x end |> process_input(opts) |> case do {match_ast, _} -> match_ast end #|> IO.inspect(label: "processed ast") #|> case do x -> IO.puts("processed -- #{Macro.to_string(x)}") ; x end end @doc false def preprocess(ast, matcher) do case matcher.(ast) do {:ok, ast} -> ast {:ambiguous, sub_ast, finalize} -> finalize.(preprocess(sub_ast, matcher)) #|> IO.inspect(label: "FINALIZED AS") :no_match -> case ast do {left, meta, right} -> ast_left = preprocess(left, matcher) ast_right = preprocess(right, matcher) {ast_left, meta, ast_right} xs when is_list(xs) -> Enum.map(xs, fn x -> preprocess(x, matcher) end) {:do, x} -> # do block {:do, preprocess(x, matcher)} x when is_atom(x) -> x end end end ### Other ### @doc false def default_color do [ atom: :cyan, string: :green, list: :default_color, boolean: :magenta, nil: :magenta, tuple: :default_color, binary: :default_color, map: :default_color ] end end