Files
n8n-openai-adapter/lib/elixir/lib/macro.ex
T

615 lines
18 KiB
Elixir

import Kernel, except: [to_string: 1]
defmodule Macro do
@moduledoc """
This module provides conveniences for working with macros.
"""
@doc """
Returns a list of binary operators. This is available
as a macro so it can be used in guard clauses.
"""
defmacro binary_ops do
[
:===, :!==,
:==, :!=, :<=, :>=,
:&&, :||, :<>, :++, :--, :**, ://, :::, :<-, :.., :|>, :=~,
:<, :>, :->,
:+, :-, :*, :/, :=, :|, :.,
:and, :or, :xor, :when, :in, :inlist, :inbits,
:<<<, :>>>, :|||, :&&&, :^^^
]
end
@doc """
Returns a list of unary operators. This is available
as a macro so it can be used in guard clauses.
"""
defmacro unary_ops do
[:!, :@, :^, :not, :+, :-, :~~~, :&]
end
@doc """
Receives an expresion representing a possible definition
and extracts its arguments. It returns a tuple with the
function name and the arguments list or `:error` if not
a valid call syntax.
This is useful for macros that want to provide the same
arguments syntax available in def/defp/defmacro and friends.
## Examples
extract_args(quote do: foo) == { :foo, [] }
extract_args(quote do: foo()) == { :foo, [] }
extract_args(quote do: foo(1, 2, 3)) == { :foo, [1, 2, 3] }
extract_args(quote do: 1.(1, 2, 3)) == :error
"""
def extract_args(expr) do
:elixir_clauses.extract_args(expr)
end
@doc """
Recursively escapes a value so it can be inserted
into a syntax tree.
One may pass `unquote: true` to `Macro.escape/2`
which leaves unquote statements unescaped, effectively
unquoting the contents on escape.
## Examples
iex> Macro.escape(:foo)
:foo
iex> Macro.escape({ :a, :b, :c })
{ :{}, [], [:a, :b, :c] }
iex> Macro.escape({ :unquote, [], [1] }, unquote: true)
1
"""
def escape(expr, opts // []) do
:elixir_quote.escape(expr, Keyword.get(opts, :unquote, false)) |> elem(0)
end
@doc %B"""
Unescape the given chars. This is the unescaping behavior
used by default in Elixir single- and double-quoted strings.
Check `unescape_binary/2` for information on how to customize
the escaping map.
In this setup, Elixir will escape the following: `\a`, `\b`,
`\d`, `\e`, `\f`, `\n`, `\r`, `\s`, `\t` and `\v`. Octals are
also escaped according to the latin1 set they represent.
This function is commonly used on sigil implementations
(like `%r`, `%b` and others).
## Examples
iex> Macro.unescape_binary("example\\n")
"example\n"
In the example above, we pass a string with `\n` escaped
and we return a version with it unescaped.
"""
def unescape_binary(chars) do
:elixir_interpolation.unescape_chars(chars)
end
@doc %B"""
Unescape the given chars according to the map given.
Check `unescape_binary/1` if you want to use the same map
as Elixir single- and double-quoted strings.
## Map
The map must be a function. The function receives an integer
representing the number of the characters it wants to unescape.
Here is the default mapping function implemented by Elixir:
def unescape_map(?a), do: ?\a
def unescape_map(?b), do: ?\b
def unescape_map(?d), do: ?\d
def unescape_map(?e), do: ?\e
def unescape_map(?f), do: ?\f
def unescape_map(?n), do: ?\n
def unescape_map(?r), do: ?\r
def unescape_map(?s), do: ?\s
def unescape_map(?t), do: ?\t
def unescape_map(?v), do: ?\v
def unescape_map(e), do: e
If the `unescape_map` function returns false. The char is
not escaped and `\` is kept in the char list.
## Octals
Octals will by default be escaped unless the map function
returns false for ?0.
## Hex
Octals will by default be escaped unless the map function
returns false for ?x.
## Examples
Using the unescape_map defined above is easy:
Macro.unescape_binary "example\\n", unescape_map(&1)
"""
def unescape_binary(chars, map) do
:elixir_interpolation.unescape_chars(chars, map)
end
@doc """
Unescape the given tokens according to the default map.
Check `unescape_binary/1` and `unescape_binary/2` for more
information about unescaping.
Only tokens that are binaries are unescaped, all others are
ignored. This function is useful when implementing your own
sigils. Check the implementation of `Kernel.sigil_b`
for examples.
"""
def unescape_tokens(tokens) do
:elixir_interpolation.unescape_tokens(tokens)
end
@doc """
Unescape the given tokens according to the given map.
Check `unescape_tokens/1` and `unescape_binary/2` for more information.
"""
def unescape_tokens(tokens, map) do
:elixir_interpolation.unescape_tokens(tokens, map)
end
@doc false
def to_binary(tree) do
IO.write "[WARNING] Macro.to_binary is deprecated, please use Macro.to_string instead\n#{Exception.format_stacktrace}"
to_string(tree)
end
@doc """
Converts the given expression to a binary.
## Examples
iex> Macro.to_string(quote do: foo.bar(1, 2, 3))
"foo.bar(1, 2, 3)"
"""
def to_string(tree)
# Variables
def to_string({ var, _, atom }) when is_atom(atom) do
atom_to_binary(var, :utf8)
end
# Aliases
def to_string({ :__aliases__, _, refs }) do
Enum.map_join(refs, ".", call_to_string(&1))
end
# Blocks
def to_string({ :__block__, _, [expr] }) do
to_string(expr)
end
def to_string({ :__block__, _, _ } = expr) do
block = adjust_new_lines block_to_string(expr), "\n "
"(\n " <> block <> "\n)"
end
# Bits containers
def to_string({ :<<>>, _, args }) do
"<<" <> Enum.map_join(args, ", ", to_string(&1)) <> ">>"
end
# Tuple containers
def to_string({ :{}, _, args }) do
"{" <> Enum.map_join(args, ", ", to_string(&1)) <> "}"
end
# List containers
def to_string({ :[], _, args }) do
"[" <> Enum.map_join(args, ", ", to_string(&1)) <> "]"
end
# Fn keyword
def to_string({ :fn, _, [[do: { :->, _, [{_, _, tuple}] } = arrow]] })
when not is_tuple(tuple) or elem(tuple, 0) != :__block__ do
"fn " <> arrow_to_string(arrow) <> " end"
end
def to_string({ :fn, _, [[do: { :->, _, [_] } = block]] }) do
"fn " <> block_to_string(block) <> "\nend"
end
def to_string({ :fn, _, [[do: block]] }) do
block = adjust_new_lines block_to_string(block), "\n "
"fn\n " <> block <> "\nend"
end
# Partial call
def to_string({ :&, _, [num] }) do
"&#{num}"
end
# left -> right
def to_string({ :->, _, _ } = arrow) do
"(" <> arrow_to_string(arrow, true) <> ")"
end
# Binary ops
def to_string({ op, _, [left, right] }) when op in binary_ops do
op_to_string(left) <> " #{op} " <> op_to_string(right)
end
# Splat when
def to_string({ :when, _, args }) do
{ left, right } = :elixir_tree_helpers.split_last(args)
"(" <> Enum.map_join(left, ", ", to_string(&1)) <> ") when " <> to_string(right)
end
# Unary ops
def to_string({ :not, _, [arg] }) do
"not " <> to_string(arg)
end
def to_string({ op, _, [arg] }) when op in unary_ops do
atom_to_binary(op, :utf8) <> to_string(arg)
end
# All other calls
def to_string({ target, _, args }) when is_list(args) do
{ list, last } = :elixir_tree_helpers.split_last(args)
case is_kw_blocks?(last) do
true -> call_to_string_with_args(target, list) <> kw_blocks_to_string(last)
false -> call_to_string_with_args(target, args)
end
end
# Two-item tuples
def to_string({ left, right }) do
to_string({ :{}, [], [left, right] })
end
# Lists
def to_string(list) when is_list(list) do
to_string({ :[], [], list })
end
# All other structures
def to_string(other), do: inspect(other, raw: true)
# Block keywords
defmacrop kw_keywords, do: [:do, :catch, :rescue, :after, :else]
defp is_kw_blocks?([_|_] = kw) do
Enum.all?(kw, match?({x, _} when x in kw_keywords, &1))
end
defp is_kw_blocks?(_), do: false
defp module_to_string(atom) when is_atom(atom), do: inspect(atom, raw: true)
defp module_to_string(other), do: call_to_string(other)
defp call_to_string(atom) when is_atom(atom), do: atom_to_binary(atom, :utf8)
defp call_to_string({ :., _, [arg] }), do: module_to_string(arg) <> "."
defp call_to_string({ :., _, [left, right] }), do: module_to_string(left) <> "." <> call_to_string(right)
defp call_to_string(other), do: to_string(other)
defp call_to_string_with_args(target, args) do
args = Enum.map_join(args, ", ", to_string(&1))
call_to_string(target) <> "(" <> args <> ")"
end
defp kw_blocks_to_string(kw) do
Enum.reduce(kw_keywords, " ", fn(x, acc) ->
case Keyword.has_key?(kw, x) do
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x))
false -> acc
end
end) <> "end"
end
defp kw_block_to_string(key, value) do
block = adjust_new_lines block_to_string(value), "\n "
atom_to_binary(key, :utf8) <> "\n " <> block <> "\n"
end
defp block_to_string({ :->, _, exprs }) do
Enum.map_join(exprs, "\n", fn({ left, _, right }) ->
left = comma_join_or_empty_paren(left, false)
left <> "->\n " <> adjust_new_lines block_to_string(right), "\n "
end)
end
defp block_to_string({ :__block__, _, exprs }) do
Enum.map_join(exprs, "\n", to_string(&1))
end
defp block_to_string(other), do: to_string(other)
defp op_to_string({ op, _, [_, _] } = expr) when op in binary_ops do
"(" <> to_string(expr) <> ")"
end
defp op_to_string(expr), do: to_string(expr)
defp arrow_to_string({ :->, _, pairs }, paren // false) do
Enum.map_join(pairs, "; ", fn({ left, _, right }) ->
left = comma_join_or_empty_paren(left, paren)
left <> "-> " <> to_string(right)
end)
end
defp comma_join_or_empty_paren([], true), do: "() "
defp comma_join_or_empty_paren([], false), do: ""
defp comma_join_or_empty_paren(left, _) do
Enum.map_join(left, ", ", to_string(&1)) <> " "
end
defp adjust_new_lines(block, replacement) do
bc <<x>> inbits block do
<< case x == ?\n do
true -> replacement
false -> <<x>>
end :: binary >>
end
end
@doc """
Receives a AST node and expands it once. The following contents are expanded:
* Macros (local or remote);
* Aliases are expanded (if possible) and return atoms;
* All pseudo-variables (__FILE__, __MODULE__, etc);
* Module attributes reader (@foo);
In case the expression cannot be expanded, it returns the expression
itself. Notice that `Macro.expand_once/2` performs the expansion just
once and it is not recursive. Check `Macro.expand/2` for expansion
until the node no longer represents a macro and `Macro.expand_all/2`
for recursive expansion.
## Examples
In the example below, we have a macro that generates a module
with a function named `name_length` that returns the length
of the module name. The value of this function will be calculated
at compilation time and not at runtime.
Consider the implementation below:
defmacro defmodule_with_length(name, do: block) do
length = length(atom_to_list(name))
quote do
defmodule unquote(name) do
def name_length, do: unquote(length)
unquote(block)
end
end
end
When invoked like this:
defmodule_with_length My.Module do
def other_function, do: ...
end
The compilation will fail because `My.Module` when quoted
is not an atom, but a syntax tree as follow:
{:__aliases__, [], [:My, :Module] }
That said, we need to expand the aliases node above to an
atom, so we can retrieve its length. Expanding the node is
not straight-forward because we also need to expand the
caller aliases. For example:
alias MyHelpers, as: My
defmodule_with_length My.Module do
def other_function, do: ...
end
The final module name will be `MyHelpers.Module` and not
`My.Module`. With `Macro.expand`, such aliases are taken
into consideration. Local and remote macros are also
expanded. We could rewrite our macro above to use this
function as:
defmacro defmodule_with_length(name, do: block) do
expanded = Macro.expand(name, __CALLER__)
length = length(atom_to_list(expanded))
quote do
defmodule unquote(name) do
def name_length, do: unquote(length)
unquote(block)
end
end
end
"""
def expand_once(aliases, env) do
expand_once(aliases, env, nil) |> elem(0)
end
defp expand_once({ :__aliases__, _, _ } = original, env, cache) do
case :elixir_aliases.expand(original, env.aliases, env.macro_aliases) do
atom when is_atom(atom) -> { atom, true, cache }
aliases ->
aliases = lc alias inlist aliases, do: (expand_once(alias, env, cache) |> elem(0))
case :lists.all(is_atom(&1), aliases) do
true -> { :elixir_aliases.concat(aliases), true, cache }
false -> { original, false, cache }
end
end
end
# Expand @ calls
defp expand_once({ :@, _, [{ name, _, args }] } = original, env, cache) when is_atom(args) or args == [] do
case (module = env.module) && Module.open?(module) do
true -> { Module.get_attribute(module, name), true, cache }
false -> { original, false, cache }
end
end
# Expand pseudo-variables
defp expand_once({ :__MODULE__, _, atom }, env, cache) when is_atom(atom),
do: { env.module, true, cache }
defp expand_once({ :__FILE__, _, atom }, env, cache) when is_atom(atom),
do: { env.file, true, cache }
defp expand_once({ :__DIR__, _, atom }, env, cache) when is_atom(atom),
do: { :filename.dirname(env.file), true, cache }
defp expand_once({ :__ENV__, _, atom }, env, cache) when is_atom(atom),
do: { env, true, cache }
# Expand possible macro import invocation
defp expand_once({ atom, line, args } = original, env, cache) when is_atom(atom) do
args = case is_atom(args) do
true -> []
false -> args
end
case not is_partial?(args) do
false -> { original, false, cache }
true ->
module = env.module
extra = if function_exported?(module, :__info__, 1) do
[{ module, module.__info__(:macros) }]
else
[]
end
cache = to_erl_env(env, cache)
expand = :elixir_dispatch.expand_import(line, { atom, length(args) }, args,
env.module, extra, cache)
case expand do
{ :ok, _, expanded } -> { expanded, true, cache }
{ :error, _ } -> { original, false, cache }
end
end
end
# Expand possible macro require invocation
defp expand_once({ { :., _, [left, right] }, line, args } = original, env, cache) when is_atom(right) do
{ receiver, _, _ } = expand_once(left, env, cache)
case is_atom(receiver) and not is_partial?(args) do
false -> { original, false, cache }
true ->
cache = to_erl_env(env, cache)
expand = :elixir_dispatch.expand_require(line, receiver, { right, length(args) },
args, env.module, cache)
case expand do
{ :ok, _receiver, expanded } -> { expanded, true, cache }
{ :error, _ } -> { original, false, cache }
end
end
end
# Anything else is just returned
defp expand_once(other, _env, cache), do: { other, false, cache }
defp to_erl_env(env, nil), do: :elixir_scope.to_erl_env(env)
defp to_erl_env(_env, cache), do: cache
defp is_partial?(args) do
:lists.any(match?({ :&, _, [_] }, &1), args)
end
@doc """
Receives a AST node and expands it until it no longer represents
a macro. Check `Macro.expand_once/2` for more information on how
expansion works and `Macro.expand_all/2` for recursive expansion.
"""
def expand(tree, env) do
expand(tree, env, nil) |> elem(0)
end
@doc false # Used internally by Elixir
def expand(tree, env, cache) do
expand_until({ tree, true, cache }, env)
end
defp expand_until({ tree, true, cache }, env) do
expand_until(expand_once(tree, env, cache), env)
end
defp expand_until({ tree, false, cache }, _env) do
{ tree, cache }
end
@doc """
Receives a AST node and expands it until it no longer represents
a macro. Then it expands all of its children recursively.
Check `Macro.expand_once/2` for more information on how expansion
works.
"""
def expand_all(tree, env) do
expand_all(tree, env, nil) |> elem(0)
end
@doc false # Used internally by Elixir
def expand_all(tree, env, cache) do
expand_all_until(expand(tree, env, cache), env)
end
defp expand_all_until({ { left, meta, right }, cache }, env) do
{ left, cache } = expand_all(left, env, cache)
{ right, cache } = expand_all(right, env, cache)
{ { left, meta, right }, cache }
end
defp expand_all_until({ { left, right }, cache }, env) do
{ left, cache } = expand_all(left, env, cache)
{ right, cache } = expand_all(right, env, cache)
{ { left, right }, cache }
end
defp expand_all_until({ list, cache }, env) when is_list(list) do
:lists.mapfoldl(expand_all(&1, env, &2), cache, list)
end
defp expand_all_until({ other, cache }, _env) do
{ other, cache }
end
@doc """
Recurs the quoted expression checking if all sub terms are
safe (i.e. they represented data structured and don't actually
evaluate code) and returns `:ok` unless a given term is unsafe,
which is returned as `{ :unsafe, term }`.
"""
def safe_term(terms) do
do_safe_term(terms) || :ok
end
defp do_safe_term({ local, _, terms }) when local in [:{}, :[], :__aliases__] do
do_safe_term(terms)
end
defp do_safe_term({ unary, _, [term] }) when unary in [:+, :-] do
do_safe_term(term)
end
defp do_safe_term({ left, right }), do: do_safe_term(left) || do_safe_term(right)
defp do_safe_term(terms) when is_list(terms), do: Enum.find_value(terms, do_safe_term(&1))
defp do_safe_term(terms) when is_tuple(terms), do: { :unsafe, terms }
defp do_safe_term(_), do: nil
end