458 lines
15 KiB
Elixir
458 lines
15 KiB
Elixir
defmodule Code do
|
|
defexception LoadError, file: nil do
|
|
def message(exception) do
|
|
"could not load #{exception.file}"
|
|
end
|
|
end
|
|
|
|
@moduledoc """
|
|
The Code module is responsible to manage code compilation,
|
|
code evaluation and code loading.
|
|
|
|
It complements (Erlang's code module)[1] to add behavior
|
|
which is specific to Elixir.
|
|
|
|
[1]: (www.erlang.org/doc/man/code.html)
|
|
|
|
"""
|
|
|
|
@doc """
|
|
Returns all the loaded files.
|
|
"""
|
|
def loaded_files do
|
|
:elixir_code_server.call :loaded
|
|
end
|
|
|
|
@doc """
|
|
Removes the given files from the loaded files list.
|
|
The modules defined in the file are not removed,
|
|
calling this function only removes it from the list,
|
|
allowing it to be required again.
|
|
"""
|
|
def unload_files(files) do
|
|
:elixir_code_server.cast { :unload_files, files }
|
|
end
|
|
|
|
@doc """
|
|
Appends a path to Erlang VM code path.
|
|
The path is expanded with `Path.expand` before added.
|
|
"""
|
|
def append_path(path) do
|
|
:code.add_pathz(Path.expand to_char_list(path))
|
|
end
|
|
|
|
@doc """
|
|
Prepends a path to Erlang VM code path.
|
|
The path is expanded with `Path.expand` before added.
|
|
"""
|
|
def prepend_path(path) do
|
|
:code.add_patha(Path.expand to_char_list(path))
|
|
end
|
|
|
|
@doc """
|
|
Deletes a path from Erlang VM code path.
|
|
The path is expanded with `Path.expand` before deleted.
|
|
"""
|
|
def delete_path(path) do
|
|
:code.del_path(Path.expand to_char_list(path))
|
|
end
|
|
|
|
@doc """
|
|
Evaluates the contents given by string. The second argument is the
|
|
binding (which should be a keyword) followed by a keyword list of
|
|
environment options. Those options can be:
|
|
|
|
* `:file` - the file to be considered in the evaluation
|
|
* `:line` - the line the script starts
|
|
* `:delegate_locals_to` - delegate local calls to the given module,
|
|
the default is to not delegate
|
|
|
|
Besides, the following scope values can be configured:
|
|
|
|
* `:aliases` - a list of tuples with the alias and its target
|
|
* `:requires` - a list of modules required
|
|
* `:functions` - a list of tuples where the first element is a module
|
|
and the second a list of imported function names and arity. The list
|
|
of function names and arity must be sorted;
|
|
* `:macros` - a list of tuples where the first element is a module
|
|
and the second a list of imported macro names and arity. The list
|
|
of function names and arity must be sorted;
|
|
|
|
Notice that setting any of the values above overrides Elixir default
|
|
values. For example, setting `:requires` to `[]`, will no longer
|
|
automatically required the `Kernel` module, in the same way setting
|
|
`:macros` will no longer auto-import `Kernel` macros as `if`, `case`,
|
|
etc.
|
|
|
|
## Examples
|
|
|
|
iex> Code.eval_string("a + b", [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
|
{ 3, [ {:a, 1}, {:b, 2} ] }
|
|
|
|
For convenience, you can my pass `__ENV__` as argument and
|
|
all imports, requires and aliases will be automatically carried
|
|
over:
|
|
|
|
iex> Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
|
|
{ 3, [ {:a, 1}, {:b, 2} ] }
|
|
|
|
"""
|
|
def eval_string(string, binding // [], opts // [])
|
|
|
|
def eval_string(string, binding, Macro.Env[] = env) do
|
|
do_eval_string(string, binding, env.to_keywords)
|
|
end
|
|
|
|
def eval_string(string, binding, opts) when is_list(opts) do
|
|
validate_eval_opts(opts)
|
|
do_eval_string(string, binding, opts)
|
|
end
|
|
|
|
defp do_eval_string(string, binding, opts) when is_list(binding) do
|
|
{ value, binding, _scope } =
|
|
:elixir.eval :unicode.characters_to_list(string), binding, opts
|
|
{ value, binding }
|
|
end
|
|
|
|
@doc """
|
|
Evaluates the quoted contents.
|
|
|
|
This function accepts a list of environment options.
|
|
Check `Code.eval_string` for more information.
|
|
|
|
## Examples
|
|
|
|
iex> contents = quote(hygiene: [vars: false], do: a + b)
|
|
...> Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
|
{ 3, [ {:a, 1}, {:b, 2} ] }
|
|
|
|
For convenience, you can my pass `__ENV__` as argument and
|
|
all options will be automatically extracted from the environment:
|
|
|
|
iex> contents = quote(hygiene: [vars: false], do: a + b)
|
|
...> Code.eval_quoted(contents, [a: 1, b: 2], __ENV__)
|
|
{ 3, [ {:a, 1}, {:b, 2} ] }
|
|
|
|
"""
|
|
def eval_quoted(quoted, binding // [], opts // [])
|
|
|
|
def eval_quoted(quoted, binding, Macro.Env[] = env) do
|
|
do_eval_quoted(quoted, binding, env.to_keywords)
|
|
end
|
|
|
|
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
|
validate_eval_opts(opts)
|
|
do_eval_quoted(quoted, binding, opts)
|
|
end
|
|
|
|
defp do_eval_quoted(quoted, binding, opts) when is_list(binding) do
|
|
{ value, binding, _scope } =
|
|
:elixir.eval_quoted [quoted], binding, opts
|
|
{ value, binding }
|
|
end
|
|
|
|
defp validate_eval_opts(opts) do
|
|
if f = opts[:functions], do: validate_imports(:functions, f)
|
|
if m = opts[:macros], do: validate_imports(:macros, m)
|
|
if a = opts[:aliases], do: validate_aliases(:aliases, a)
|
|
if r = opts[:requires], do: validate_requires(:requires, r)
|
|
end
|
|
|
|
defp validate_requires(kind, requires) do
|
|
valid = is_list(requires) and Enum.all?(requires, is_atom(&1))
|
|
|
|
unless valid do
|
|
raise ArgumentError, "expected :#{kind} option given to eval in the format: [module]"
|
|
end
|
|
end
|
|
|
|
defp validate_aliases(kind, aliases) do
|
|
valid = is_list(aliases) and Enum.all?(aliases, fn { k, v } ->
|
|
is_atom(k) and is_atom(v)
|
|
end)
|
|
|
|
unless valid do
|
|
raise ArgumentError, "expected :#{kind} option given to eval in the format: [{ module, module }]"
|
|
end
|
|
end
|
|
|
|
defp validate_imports(kind, imports) do
|
|
valid = is_list(imports) and Enum.all?(imports, fn { k, v } ->
|
|
is_atom(k) and is_list(v) and Enum.all?(v, fn { name, arity } ->
|
|
is_atom(name) and is_integer(arity)
|
|
end)
|
|
end)
|
|
|
|
unless valid do
|
|
raise ArgumentError, "expected :#{kind} option given to eval in the format: [{ module, [{ name, arity }] }]"
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def string_to_ast(string, opts // []) do
|
|
IO.write "[WARNING] Code.string_to_ast is deprecated, please use Code.string_to_quoted instead\n#{Exception.format_stacktrace}"
|
|
string_to_quoted(string, opts)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given string to quoted form. It returns `{ :ok, quoted_form }`
|
|
if it succeeds, `{ :error, { line, error, token } }` otherwise.
|
|
|
|
## Options
|
|
|
|
* `:file` - The filename to be used in stacktraces
|
|
and the file reported in the __ENV__ variable.
|
|
|
|
* `:line` - The line reported in the __ENV__ variable.
|
|
|
|
* `:existing_atoms_only` - When true, raises an error
|
|
when non-existing atoms are found by the tokenizer.
|
|
|
|
## Macro.to_string/1
|
|
|
|
The opposite of converting a string to its quoted form is
|
|
`Macro.to_string`, which converts a quoted form to a string/binary
|
|
representation.
|
|
"""
|
|
def string_to_quoted(string, opts // []) do
|
|
file = Keyword.get opts, :file, "nofile"
|
|
line = Keyword.get opts, :line, 1
|
|
res = :elixir_translator.forms(:unicode.characters_to_list(string), line, file, opts)
|
|
|
|
case res do
|
|
{ :ok, forms } -> { :ok, unpack_quote(line, forms) }
|
|
_ -> res
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def string_to_ast!(string, opts // []) do
|
|
IO.write "[WARNING] Code.string_to_ast! is deprecated, please use Code.string_to_quoted! instead\n#{Exception.format_stacktrace}"
|
|
string_to_quoted!(string, opts)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given string to quoted form. It returns the ast if it succeeds,
|
|
raises an exception otherwise. The exception is a TokenMissingError
|
|
in case a token is missing (usually because the expression is incomplete),
|
|
SyntaxError otherwise.
|
|
|
|
Check `Code.string_to_quoted/2` for options information.
|
|
"""
|
|
def string_to_quoted!(string, opts // []) do
|
|
file = Keyword.get opts, :file, "nofile"
|
|
line = Keyword.get opts, :line, 1
|
|
res = :elixir_translator.forms!(:unicode.characters_to_list(string), line, file, opts)
|
|
unpack_quote(line, res)
|
|
end
|
|
|
|
defp unpack_quote(_line, []), do: nil
|
|
defp unpack_quote(_line, [forms]) when not is_list(forms), do: forms
|
|
defp unpack_quote(line, forms), do: { :__block__, [line: line], forms }
|
|
|
|
@doc """
|
|
Loads the given `file`. Accepts `relative_to` as an argument to tell where
|
|
the file is located. If the file was already required/loaded, loads it again.
|
|
It returns a list of tuples { ModuleName, <<byte_code>> }, one tuple for each
|
|
module defined in the file.
|
|
|
|
Notice that if `load_file` is invoked by different processes
|
|
concurrently, the target file will be invoked concurrently
|
|
in many times. I.e. if `load_file` is called N times with
|
|
a given file, the given file will be loaded N times. Check
|
|
`require_file` if you don't want a file to be loaded concurrently.
|
|
"""
|
|
def load_file(file, relative_to // nil) when is_binary(file) do
|
|
file = find_file(file, relative_to)
|
|
:elixir_code_server.call { :acquire, file }
|
|
loaded = :elixir_compiler.file file
|
|
:elixir_code_server.cast { :loaded, file }
|
|
loaded
|
|
end
|
|
|
|
@doc """
|
|
Requires the given `file`. Accepts `relative_to` as an argument to tell where
|
|
the file is located. The return value is the same as that of `load_file`. If
|
|
the file was already required/loaded, doesn't do anything and returns nil.
|
|
|
|
Notice that if `require_file` is invoked by different processes concurrently,
|
|
the first process to invoke `require_file` acquires a lock and the remaining
|
|
ones will block until the file is available. I.e. if `require_file` is called
|
|
N times with a given file, it will be loaded only once. The first process to
|
|
call `require_file` will get the list of loaded modules, others will get nil.
|
|
|
|
Check `load_file` if you want a file to be loaded concurrently.
|
|
"""
|
|
def require_file(file, relative_to // nil) when is_binary(file) do
|
|
file = find_file(file, relative_to)
|
|
|
|
case :elixir_code_server.call({ :acquire, file }) do
|
|
:loaded ->
|
|
nil
|
|
{ :queued, ref } ->
|
|
receive do { :elixir_code_server, ^ref, :loaded } -> nil end
|
|
:proceed ->
|
|
loaded = :elixir_compiler.file file
|
|
:elixir_code_server.cast { :loaded, file }
|
|
loaded
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Loads the compilation options from the code server.
|
|
Check `compiler_options/1` for more information.
|
|
"""
|
|
def compiler_options do
|
|
:elixir_code_server.call :compiler_options
|
|
end
|
|
|
|
@doc """
|
|
Sets compilation options. Those options are global
|
|
since they are stored by Elixir's Code Server.
|
|
|
|
Available options are:
|
|
|
|
* `:docs` - when true, retain documentation in the compiled module.
|
|
True by default;
|
|
|
|
* `:debug_info` - when true, retain debug information in the compiled module.
|
|
This allows a developer to reconstruct the original source
|
|
code, for such reasons, false by default;
|
|
|
|
* `:ignore_module_conflict` - when true, override modules that were already defined
|
|
without raising errors, false by default;
|
|
|
|
* `:warnings_as_errors` - cause compilation to fail when warnings are spewed;
|
|
|
|
"""
|
|
def compiler_options(opts) do
|
|
:elixir_code_server.cast { :compiler_options, opts }
|
|
end
|
|
|
|
@doc """
|
|
Compiles the given string and returns a list of tuples where
|
|
the first element is the module name and the second one is its
|
|
binary.
|
|
|
|
For compiling many files at once, check `Kernel.ParallelCompiler`.
|
|
"""
|
|
def compile_string(string, file // "nofile") when is_binary(file) do
|
|
:elixir_compiler.string :unicode.characters_to_list(string), file
|
|
end
|
|
|
|
@doc """
|
|
Compiles the quoted expression and returns a list of tuples where
|
|
the first element is the module name and the second one is its
|
|
binary.
|
|
"""
|
|
def compile_quoted(quoted, file // "nofile") when is_binary(file) do
|
|
:elixir_compiler.quoted [quoted], file
|
|
end
|
|
|
|
@doc """
|
|
Ensures the given module is loaded. If the module is already
|
|
loaded, it works as no-op. If the module was not loaded yet,
|
|
it tries to load it.
|
|
|
|
If it succeeds loading the module anyhow, it returns
|
|
`{ :module, module }`. If not, returns `{ :error, reason }` with
|
|
the error reason.
|
|
|
|
## Code loading on the Erlang VM
|
|
|
|
Erlang has two modes to load code: interactive and embedded.
|
|
|
|
By default, the Erlang VM runs on interactive mode, where modules
|
|
are loaded as needed. In embedded mode the opposite happens, as all
|
|
modules need to be loaded upfront or explicitly.
|
|
|
|
Therefore, this function is useful to check if a module is loaded
|
|
before using it and react accordingly. For example, the `URI` module
|
|
uses this function to check if a specific parser exists for a given
|
|
URI scheme.
|
|
|
|
## Code.ensure_compiled
|
|
|
|
Elixir also contains an `ensure_compiled/1` function that is a
|
|
superset of `ensure_loaded/1`.
|
|
|
|
Since Elixir's compilation happens in parallel, in some situations
|
|
you may need to use a module but it was not compiled yet, therefore
|
|
it can't even be loaded.
|
|
|
|
`ensure_compiled/1` puts a halt in the current process until the
|
|
module we are depending on is available.
|
|
|
|
In most of the cases, `ensure_loaded` is enough. `ensure_compiled`
|
|
must be used just in same rare conditions, usually involving macros
|
|
that needs to invoke a module for callback information.
|
|
"""
|
|
def ensure_loaded(module) when is_atom(module) do
|
|
:code.ensure_loaded(module)
|
|
end
|
|
|
|
@doc """
|
|
Similar to `ensure_loaded/1`, but returns a boolean in case
|
|
it could be ensured or not.
|
|
"""
|
|
def ensure_loaded?(module) do
|
|
match?({ :module, ^module }, ensure_loaded(module))
|
|
end
|
|
|
|
@doc """
|
|
Ensures the given module is compiled and loaded. If the module
|
|
is already loaded, it works as no-op. If the module was not
|
|
loaded yet, it checks if it needs to be compiled first and just
|
|
then tries to load it.
|
|
|
|
If it succeeds loading the module anyhow, it returns
|
|
`{ :module, module }`. If not, returns `{ :error, reason }` with
|
|
the error reason.
|
|
|
|
Check `ensure_loaded/1` for more information on module loading
|
|
and when to use `ensure_loaded/1` or `ensure_compiled/1`.
|
|
"""
|
|
def ensure_compiled(module) when is_atom(module) do
|
|
case :code.ensure_loaded(module) do
|
|
{ :error, :nofile } = error ->
|
|
case :erlang.get(:elixir_ensure_compiled) do
|
|
:undefined -> error
|
|
_ ->
|
|
try do
|
|
module.__info__(:module)
|
|
{ :module, module }
|
|
rescue
|
|
UndefinedFunctionError -> error
|
|
end
|
|
end
|
|
other -> other
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Similar to `ensure_compiled/1`, but returns a boolean in case
|
|
it could be ensured or not.
|
|
"""
|
|
def ensure_compiled?(module) do
|
|
match?({ :module, ^module }, ensure_compiled(module))
|
|
end
|
|
|
|
## Helpers
|
|
|
|
# Finds the file given the relative_to path.
|
|
# If the file is found, returns its path in binary, fails otherwise.
|
|
defp find_file(file, relative_to) do
|
|
file = if relative_to do
|
|
Path.expand(file, relative_to)
|
|
else
|
|
Path.expand(file)
|
|
end
|
|
|
|
if File.regular?(file) do
|
|
file
|
|
else
|
|
raise LoadError, file: file
|
|
end
|
|
end
|
|
end
|