559 lines
17 KiB
Elixir
559 lines
17 KiB
Elixir
defmodule Code do
|
|
@moduledoc """
|
|
Utilities for managing code compilation, code evaluation and code loading.
|
|
|
|
This module complements [Erlang's code module](http://www.erlang.org/doc/man/code.html)
|
|
to add behaviour which is specific to Elixir.
|
|
"""
|
|
|
|
@doc """
|
|
List all loaded files.
|
|
"""
|
|
def loaded_files do
|
|
:elixir_code_server.call :loaded
|
|
end
|
|
|
|
@doc """
|
|
Remove files from the loaded files list.
|
|
|
|
The modules defined in the file are not removed;
|
|
calling this function only removes them from the list,
|
|
allowing them to be required again.
|
|
"""
|
|
def unload_files(files) do
|
|
:elixir_code_server.cast {:unload_files, files}
|
|
end
|
|
|
|
@doc """
|
|
Append a path to the Erlang VM code path.
|
|
|
|
The path is expanded with `Path.expand/1` before being appended.
|
|
"""
|
|
def append_path(path) do
|
|
:code.add_pathz(to_char_list(Path.expand path))
|
|
end
|
|
|
|
@doc """
|
|
Prepend a path to the Erlang VM code path.
|
|
|
|
The path is expanded with `Path.expand/1` before being prepended.
|
|
"""
|
|
def prepend_path(path) do
|
|
:code.add_patha(to_char_list(Path.expand path))
|
|
end
|
|
|
|
@doc """
|
|
Delete a path from the Erlang VM code path.
|
|
|
|
The path is expanded with `Path.expand/1` before being deleted.
|
|
"""
|
|
def delete_path(path) do
|
|
:code.del_path(to_char_list(Path.expand path))
|
|
end
|
|
|
|
@doc """
|
|
Re-add paths given to the command line to keep their position
|
|
on the overall code path.
|
|
|
|
Some tools may change the code path by prepending new items but
|
|
still want the paths given by the user to have higher priority.
|
|
Calling this function guarantees the paths are re-added on
|
|
top of the user given ones.
|
|
"""
|
|
@spec readd_paths() :: :ok
|
|
def readd_paths() do
|
|
{pa, pz} = :elixir_code_server.call(:paths)
|
|
:code.add_pathsa(pa)
|
|
:code.add_pathsz(pz)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Evaluate the contents given by `string`.
|
|
|
|
The `binding` argument is a keyword list of variable bindings.
|
|
The `opts` argument is a keyword list of environment options.
|
|
|
|
Those options can be:
|
|
|
|
* `:file` - the file to be considered in the evaluation
|
|
* `:line` - the line on which the script starts
|
|
* `:delegate_locals_to` - delegate local calls to the given module,
|
|
the default is to not delegate
|
|
|
|
Additionally, 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's default
|
|
values. For example, setting `:requires` to `[]`, will no longer
|
|
automatically require the `Kernel` module; in the same way setting
|
|
`:macros` will no longer auto-import `Kernel` macros like `if`, `case`,
|
|
etc.
|
|
|
|
Returns a tuple of the form `{value, binding}`,
|
|
where `value` is the value returned from evaluating `string`.
|
|
If an error occurs while evaluating `string` an exception will be raised.
|
|
|
|
`binding` is a keyword list with the value of all variable bindings
|
|
after evaluating `string`. The binding key is usually an atom, but it
|
|
may be a tuple for variables defined in a different context.
|
|
|
|
## Examples
|
|
|
|
iex> Code.eval_string("a + b", [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
|
{3, [a: 1, b: 2]}
|
|
|
|
iex> Code.eval_string("c = a + b", [a: 1, b: 2], __ENV__)
|
|
{3, [a: 1, b: 2, c: 3]}
|
|
|
|
iex> Code.eval_string("a = a + b", [a: 1, b: 2])
|
|
{3, [a: 3, b: 2]}
|
|
|
|
For convenience, you can pass `__ENV__` as the `opts` argument and
|
|
all imports, requires and aliases defined in the current environment
|
|
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
|
|
{value, binding, _env, _scope} = :elixir.eval to_char_list(string), binding, Map.to_list(env)
|
|
{value, binding}
|
|
end
|
|
|
|
def eval_string(string, binding, opts) when is_list(opts) do
|
|
validate_eval_opts(opts)
|
|
{value, binding, _env, _scope} = :elixir.eval to_char_list(string), binding, opts
|
|
{value, binding}
|
|
end
|
|
|
|
@doc """
|
|
Evaluate the quoted contents.
|
|
|
|
See `eval_string/3` for a description of arguments and return values.
|
|
|
|
## Examples
|
|
|
|
iex> contents = quote(do: var!(a) + var!(b))
|
|
iex> Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
|
{3, [a: 1, b: 2]}
|
|
|
|
For convenience, you can pass `__ENV__` as the `opts` argument and
|
|
all options will be automatically extracted from the current environment:
|
|
|
|
iex> contents = quote(do: var!(a) + var!(b))
|
|
iex> 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
|
|
{value, binding, _env, _scope} = :elixir.eval_quoted quoted, binding, Map.to_list(env)
|
|
{value, binding}
|
|
end
|
|
|
|
def eval_quoted(quoted, binding, opts) when is_list(opts) do
|
|
validate_eval_opts(opts)
|
|
{value, binding, _env, _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 """
|
|
Convert the given string to its quoted form.
|
|
|
|
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/2
|
|
|
|
The opposite of converting a string to its quoted form is
|
|
`Macro.to_string/2`, which converts a quoted form to a string/binary
|
|
representation.
|
|
"""
|
|
def string_to_quoted(string, opts \\ []) when is_list(opts) do
|
|
file = Keyword.get opts, :file, "nofile"
|
|
line = Keyword.get opts, :line, 1
|
|
:elixir.string_to_quoted(to_char_list(string), line, file, opts)
|
|
end
|
|
|
|
@doc """
|
|
Convert the given string to its 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 `string_to_quoted/2` for options information.
|
|
"""
|
|
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
|
|
file = Keyword.get opts, :file, "nofile"
|
|
line = Keyword.get opts, :line, 1
|
|
:elixir.string_to_quoted!(to_char_list(string), line, file, opts)
|
|
end
|
|
|
|
@doc """
|
|
Evals the given file.
|
|
|
|
Accepts `relative_to` as an argument to tell where the file is located.
|
|
|
|
While `load_file` loads a file and returns the loaded modules and their
|
|
byte code, `eval_file` simply evalutes the file contents and returns the
|
|
evaluation result and its bindings.
|
|
"""
|
|
def eval_file(file, relative_to \\ nil) do
|
|
file = find_file(file, relative_to)
|
|
eval_string File.read!(file), [], [file: file, line: 1]
|
|
end
|
|
|
|
@doc """
|
|
Load 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 loaded concurrently many times. Check `require_file/2`
|
|
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/2`. 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/2` if you want a file to be loaded multiple times.
|
|
"""
|
|
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 """
|
|
Gets 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 """
|
|
Returns a list with the available compiler options.
|
|
|
|
See `Code.compiler_options/1` for more info.
|
|
"""
|
|
def available_compiler_options do
|
|
[:docs, :debug_info, :ignore_module_conflict, :warnings_as_errors]
|
|
end
|
|
|
|
@doc """
|
|
Sets compilation options.
|
|
|
|
These 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, `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
|
|
generated
|
|
|
|
"""
|
|
def compiler_options(opts) do
|
|
{opts, bad} = Keyword.split(opts, available_compiler_options)
|
|
if bad != [] do
|
|
bad = bad |> Keyword.keys |> Enum.join(", ")
|
|
raise ArgumentError, message: "unknown compiler options: #{bad}"
|
|
end
|
|
:elixir_code_server.cast {:compiler_options, opts}
|
|
end
|
|
|
|
@doc """
|
|
Compiles the given string.
|
|
|
|
Returns a list of tuples where the first element is the module name
|
|
and the second one is its byte code (as a binary).
|
|
|
|
For compiling many files at once, check `Kernel.ParallelCompiler.files/2`.
|
|
"""
|
|
def compile_string(string, file \\ "nofile") when is_binary(file) do
|
|
:elixir_compiler.string to_char_list(string), file
|
|
end
|
|
|
|
@doc """
|
|
Compiles the quoted expression.
|
|
|
|
Returns a list of tuples where the first element is the module name and
|
|
the second one is its byte code (as a 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, this works as no-op. If the module
|
|
was not yet loaded, it tries to load it.
|
|
|
|
If it succeeds loading the module, 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 in 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 used to check if a module is loaded
|
|
before using it and allows one to 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/1`
|
|
|
|
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 that was not yet compiled, therefore
|
|
it can't even be loaded.
|
|
|
|
`ensure_compiled/1` halts the current process until the
|
|
module we are depending on is available.
|
|
|
|
In most cases, `ensure_loaded/1` is enough. `ensure_compiled/1`
|
|
must be used in rare cases, usually involving macros that need to
|
|
invoke a module for callback information.
|
|
"""
|
|
def ensure_loaded(module) when is_atom(module) do
|
|
:code.ensure_loaded(module)
|
|
end
|
|
|
|
@doc """
|
|
Ensures the given module is loaded.
|
|
|
|
Similar to `ensure_loaded/1`, but returns `true` if the module
|
|
is already loaded or was successfully loaded. Returns `false`
|
|
otherwise.
|
|
"""
|
|
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 then
|
|
tries to load it.
|
|
|
|
If it succeeds loading the module, 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 """
|
|
Ensures the given module is compiled and loaded.
|
|
|
|
Similar to `ensure_compiled/1`, but returns `true` if the module
|
|
is already loaded or was successfully loaded and compiled.
|
|
Returns `false` otherwise.
|
|
"""
|
|
def ensure_compiled?(module) do
|
|
match?({:module, ^module}, ensure_compiled(module))
|
|
end
|
|
|
|
@doc """
|
|
Returns the docs for the given module.
|
|
|
|
When given a module name, it finds its BEAM code and reads the docs from it.
|
|
|
|
When given a path to a .beam file, it will load the docs directly from that
|
|
file.
|
|
|
|
The return value depends on the `kind` value:
|
|
|
|
* `:docs` - list of all docstrings attached to functions and macros
|
|
using the `@doc` attribute
|
|
|
|
* `:moduledoc` - tuple `{<line>, <doc>}` where `line` is the line on
|
|
which module definition starts and `doc` is the string
|
|
attached to the module using the `@moduledoc` attribute
|
|
|
|
* `:all` - a keyword list with both `:docs` and `:moduledoc`
|
|
|
|
"""
|
|
def get_docs(module, kind) when is_atom(module) do
|
|
case :code.get_object_code(module) do
|
|
{_module, bin, _beam_path} ->
|
|
do_get_docs(bin, kind)
|
|
|
|
:error -> nil
|
|
end
|
|
end
|
|
|
|
def get_docs(binpath, kind) when is_binary(binpath) do
|
|
do_get_docs(String.to_char_list(binpath), kind)
|
|
end
|
|
|
|
@docs_chunk 'ExDc'
|
|
|
|
defp do_get_docs(bin_or_path, kind) do
|
|
case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
|
|
{:ok, {_module, [{@docs_chunk, bin}]}} ->
|
|
lookup_docs(:erlang.binary_to_term(bin), kind)
|
|
|
|
{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} -> nil
|
|
end
|
|
end
|
|
|
|
defp lookup_docs({:elixir_docs_v1, docs}, kind),
|
|
do: do_lookup_docs(docs, kind)
|
|
|
|
# unsupported chunk version
|
|
defp lookup_docs(_, _), do: nil
|
|
|
|
defp do_lookup_docs(docs, :all), do: docs
|
|
defp do_lookup_docs(docs, kind) when kind in [:docs, :moduledoc],
|
|
do: Keyword.get(docs, kind)
|
|
|
|
## 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 Code.LoadError, file: file
|
|
end
|
|
end
|
|
end
|