Current section

Files

Jump to
hologram lib hologram compiler ir.ex
Raw

lib/hologram/compiler/ir.ex

defmodule Hologram.Compiler.IR do
@moduledoc false
alias Hologram.Commons.AtomUtils
alias Hologram.Commons.SystemUtils
alias Hologram.Compiler.AST
alias Hologram.Compiler.Context
alias Hologram.Compiler.IR
alias Hologram.Compiler.Transformer
@type t ::
IR.AnonymousFunctionCall.t()
| IR.AnonymousFunctionType.t()
| IR.AtomType.t()
| IR.BitstringSegment.t()
| IR.BitstringType.t()
| IR.Block.t()
| IR.Case.t()
| IR.Clause.t()
| IR.Comprehension.t()
| IR.ComprehensionFilter.t()
| IR.Cond.t()
| IR.CondClause.t()
| IR.ConsOperator.t()
| IR.DotOperator.t()
| IR.FloatType.t()
| IR.FunctionClause.t()
| IR.FunctionDefinition.t()
| IR.IgnoredExpression.t()
| IR.IntegerType.t()
| IR.ListType.t()
| IR.LocalFunctionCall.t()
| IR.MapType.t()
| IR.MatchOperator.t()
| IR.MatchPlaceholder.t()
| IR.ModuleAttributeOperator.t()
| IR.ModuleDefinition.t()
| IR.PIDType.t()
| IR.PinOperator.t()
| IR.PortType.t()
| IR.ReferenceType.t()
| IR.RemoteFunctionCall.t()
| IR.StringType.t()
| IR.Try.t()
| IR.TryCatchClause.t()
| IR.TryRescueClause.t()
| IR.TupleType.t()
| IR.Variable.t()
| IR.With.t()
defmodule AnonymousFunctionCall do
@moduledoc false
defstruct [:function, :args]
@type t :: %__MODULE__{function: IR.t(), args: list(IR.t())}
end
defmodule AnonymousFunctionType do
@moduledoc false
defstruct [:arity, :captured_function, :captured_module, :clauses]
@type t :: %__MODULE__{
arity: integer,
captured_function: atom | nil,
captured_module: module | nil,
clauses: list(IR.FunctionClause.t())
}
end
defmodule AtomType do
@moduledoc false
defstruct [:value]
@type t :: %__MODULE__{value: atom}
end
defmodule BitstringSegment do
@moduledoc false
defstruct [:value, :modifiers]
@type(
modifier :: {:endianness, :big | :little | :native} | {:signedness, :signed | :unsigned},
{:size, IR.t()},
{:type, :binary | :bitstring | :float | :integer | :utf8 | :utf16 | :utf32},
{:unit, integer}
)
@type t :: %__MODULE__{value: IR.t(), modifiers: list(modifier)}
end
defmodule BitstringType do
@moduledoc false
defstruct [:segments]
@type t :: %__MODULE__{segments: list(IR.BitstringSegment.t())}
end
defmodule Block do
@moduledoc false
defstruct [:expressions]
@type t :: %__MODULE__{expressions: list(IR.t())}
end
defmodule Case do
@moduledoc false
defstruct [:condition, :clauses]
@type t :: %__MODULE__{condition: IR.t(), clauses: list(IR.Clause.t())}
end
defmodule Clause do
@moduledoc false
defstruct [:match, :guards, :body]
@type t :: %__MODULE__{match: IR.t(), guards: list(IR.t()), body: IR.Block.t()}
end
defmodule Comprehension do
@moduledoc false
defstruct [:generators, :filters, :collectable, :unique, :mapper, :reducer]
@type t :: %__MODULE__{
generators: list(IR.Clause.t()),
filters: list(IR.ComprehensionFilter.t()),
collectable: IR.t(),
unique: %IR.AtomType{value: boolean},
mapper: IR.Block.t() | nil,
reducer:
%{
initial_value: IR.t(),
clauses: list(IR.Clause.t())
}
| nil
}
end
defmodule ComprehensionFilter do
@moduledoc false
defstruct [:expression]
@type t :: %__MODULE__{expression: IR.t()}
end
defmodule Cond do
@moduledoc false
defstruct [:clauses]
@type t :: %__MODULE__{clauses: list(IR.CondClause.t())}
end
defmodule CondClause do
@moduledoc false
defstruct [:condition, :body]
@type t :: %__MODULE__{condition: IR.t(), body: IR.Block.t()}
end
defmodule ConsOperator do
@moduledoc false
defstruct [:head, :tail]
@type t :: %__MODULE__{head: IR.t(), tail: IR.t()}
end
defmodule DotOperator do
@moduledoc false
defstruct [:left, :right]
@type t :: %__MODULE__{left: IR.t(), right: IR.t()}
end
defmodule FloatType do
@moduledoc false
defstruct [:value]
@type t :: %__MODULE__{value: float}
end
defmodule FunctionClause do
@moduledoc false
defstruct [:params, :guards, :body]
@type t :: %__MODULE__{params: list(IR.t()), guards: list(IR.t()), body: IR.Block.t()}
end
defmodule FunctionDefinition do
@moduledoc false
defstruct [:name, :arity, :visibility, :clause]
@type t :: %__MODULE__{
name: atom,
arity: integer,
visibility: :public | :private,
clause: IR.FunctionClause.t()
}
end
defmodule IgnoredExpression do
@moduledoc false
defstruct [:type]
@type t :: %__MODULE__{type: :public_macro_definition | :private_macro_definition}
end
defmodule IntegerType do
@moduledoc false
defstruct [:value]
@type t :: %__MODULE__{value: integer}
end
defmodule ListType do
@moduledoc false
defstruct [:data]
@type t :: %__MODULE__{data: list(IR.t())}
end
defmodule LocalFunctionCall do
@moduledoc false
defstruct [:function, :args]
@type t :: %__MODULE__{function: atom, args: list(IR.t())}
end
defmodule MapType do
@moduledoc false
defstruct [:data]
@type t :: %__MODULE__{data: list({IR.t(), IR.t()})}
end
defmodule MatchOperator do
@moduledoc false
defstruct [:left, :right]
@type t :: %__MODULE__{left: IR.t(), right: IR.t()}
end
defmodule MatchPlaceholder do
@moduledoc false
defstruct []
@type t :: %__MODULE__{}
end
defmodule ModuleAttributeOperator do
@moduledoc false
defstruct [:name]
@type t :: %__MODULE__{name: atom}
end
defmodule ModuleDefinition do
@moduledoc false
defstruct [:module, :body]
@type t :: %__MODULE__{module: IR.AtomType.t(), body: IR.Block.t()}
end
defmodule PIDType do
@moduledoc false
defstruct [:value]
@type t :: %__MODULE__{value: pid}
end
defmodule PinOperator do
@moduledoc false
defstruct [:variable]
@type t :: %__MODULE__{variable: IR.Variable.t()}
end
defmodule PortType do
@moduledoc false
defstruct [:value]
@type t :: %__MODULE__{value: port}
end
defmodule ReferenceType do
@moduledoc false
defstruct [:value]
@type t :: %__MODULE__{value: reference}
end
defmodule RemoteFunctionCall do
@moduledoc false
defstruct [:module, :function, :args]
@type t :: %__MODULE__{module: IR.t(), function: atom, args: list(IR.t())}
end
defmodule StringType do
@moduledoc false
defstruct [:value]
@type t :: %__MODULE__{value: String.t()}
end
defmodule Try do
@moduledoc false
defstruct [:body, :rescue_clauses, :catch_clauses, :else_clauses, :after_block]
@type t :: %__MODULE__{
body: IR.Block.t(),
rescue_clauses: list(IR.TryRescueClause.t()),
catch_clauses: list(IR.TryCatchClause.t()),
else_clauses: list(IR.Clause.t()),
after_block: IR.Block.t()
}
end
defmodule TryCatchClause do
@moduledoc false
defstruct [:kind, :value, :guards, :body]
@type t :: %__MODULE__{
kind: IR.t() | nil,
value: IR.t(),
guards: list(IR.t()),
body: IR.Block.t()
}
end
defmodule TryRescueClause do
@moduledoc false
defstruct [:variable, :modules, :body]
@type t :: %__MODULE__{variable: atom, modules: list(module), body: IR.Block.t()}
end
defmodule TupleType do
@moduledoc false
defstruct [:data]
@type t :: %__MODULE__{data: list(IR.t())}
end
defmodule Variable do
@moduledoc false
defstruct [:name, :version]
@type t :: %__MODULE__{name: atom, version: integer | nil}
end
# TODO: finish implementing
defmodule With do
@moduledoc false
defstruct []
@type t :: %__MODULE__{}
end
@doc """
Aggregates function clauses from a module definition.
## Returns
A list, where each item is in the format:
{{function_name, arity}, {visibility, [clause_1, clause_2, ...]}}
## Example
iex> module_def = %ModuleDefinition{...}
iex> aggregate_module_funs(module_def)
[
{{:my_function_1, 3}, {:public, [%IR.FunctionClause{...}, %IR.FunctionClause{...}]}},
{{:my_function_2, 1}, {:private, [%IR.FunctionClause{...}]}}
]
"""
@spec aggregate_module_funs(ModuleDefinition.t()) ::
list({{atom, non_neg_integer}, {:public | :private, list(FunctionClause.t())}})
def aggregate_module_funs(module_def) do
module_def.body.expressions
|> Enum.reduce(%{}, fn
%IR.FunctionDefinition{name: name, arity: arity, visibility: visibility, clause: clause},
acc ->
key = {name, arity}
if acc[key] do
{visibility, clauses} = acc[key]
%{acc | key => {visibility, [clause | clauses]}}
else
Map.put(acc, key, {visibility, [clause]})
end
_expr, acc ->
acc
end)
|> Enum.map(fn {key, {visibility, clauses}} -> {key, {visibility, Enum.reverse(clauses)}} end)
end
@doc """
Returns Hologram IR for the given Elixir source code.
## Examples
iex> for_code("my_fun(1, 2)")
%IR.LocalFunctionCall{function: :my_fun, args: [%IR.IntegerType{value: 1}, %IR.IntegerType{value: 2}]}
"""
@spec for_code(binary, Context.t()) :: IR.t()
def for_code(code, %Context{} = context) do
code
|> AST.for_code()
|> Transformer.transform(context)
end
@doc """
Returns Hologram IR of the given module.
Specifying the module's BEAM path makes the call faster.
## Examples
iex> for_module(MyModule)
%IR.ModuleDefinition{module: MyModule, body: %IR.Block{expressions: [...]}}
"""
@spec for_module(module, charlist | nil) :: IR.t()
def for_module(module, beam_path \\ nil) do
module
|> AST.for_module(beam_path)
|> Transformer.transform(%Context{module: module})
end
@doc """
Returns Hologram IR for the given Elixir term.
If the term can be represented in IR then its value is returned in the shape of {:ok, ir}.
If the term can't be represented in IR then an error message is returned in the shape of {:error, message}.
## Examples
iex> my_var = 123
iex> for_term(my_var)
{:ok, %IR.IntegerType{value: 123}}
"""
@spec for_term(any) :: {:ok, IR.t()} | {:error, String.t()}
def for_term(term) do
{:ok, for_term!(term)}
rescue
e in ArgumentError ->
{:error, e.message}
end
@doc """
Returns Hologram IR for the given Elixir term, erroring out if the term can't be represented in IR.
## Examples
iex> my_var = 123
iex> for_term!(my_var)
%IR.IntegerType{value: 123}
"""
@spec for_term!(any) :: IR.t()
def for_term!(term)
# credo:disable-for-lines:34 Credo.Check.Refactor.CyclomaticComplexity
def for_term!(term) when is_function(term) do
function_info = Function.info(term)
cond do
function_info[:type] == :external ->
build_function_capture_ir(term, function_info[:module], function_info[:name])
SystemUtils.otp_version() >= 25 && function_info[:type] == :local &&
!AtomUtils.starts_with?(function_info[:name], "-") ->
function_info[:module]
|> Function.capture(function_info[:name], function_info[:arity])
|> build_function_capture_ir(function_info[:module], function_info[:name])
SystemUtils.otp_version() < 25 && function_info[:type] == :local &&
AtomUtils.starts_with?(function_info[:name], "-fun.") ->
regex = ~r'^\-fun\.(.+)/[0-9]+\-$'
[_full_match, function_str] = Regex.run(regex, to_string(function_info[:name]))
function = String.to_existing_atom(function_str)
function_info[:module]
|> Function.capture(function, function_info[:arity])
|> build_function_capture_ir(function_info[:module], function)
true ->
message =
if SystemUtils.otp_version() >= 23 do
"term contains a function that is not a named function capture"
else
"term contains a function that is not a remote function capture"
end
raise ArgumentError, message: message
end
end
def for_term!(term) when is_bitstring(term) do
term
|> Macro.escape()
|> Transformer.transform(%Context{})
end
def for_term!(term) when is_list(term) do
data = Enum.map(term, &for_term!/1)
%IR.ListType{data: data}
end
def for_term!(term) when is_map(term) do
data =
term
|> Map.to_list()
|> Enum.map(fn {key, value} -> {for_term!(key), for_term!(value)} end)
%IR.MapType{data: data}
end
def for_term!(term) when is_tuple(term) do
data =
term
|> Tuple.to_list()
|> Enum.map(&for_term!/1)
%IR.TupleType{data: data}
end
# atom, float, integer, pid, port, reference
def for_term!(term) do
Transformer.transform(term, %Context{})
end
@doc """
Walks the IR tree checking whether any function call matches the given set of MFAs.
Stops at nested `AnonymousFunctionType` nodes (they are independent scopes).
"""
@spec has_call_to?(any, module, MapSet.t()) :: boolean
def has_call_to?(term, module, mfas)
def has_call_to?(%IR.AnonymousFunctionType{}, _module, _mfas), do: false
def has_call_to?(%IR.LocalFunctionCall{function: function, args: args}, module, mfas) do
MapSet.member?(mfas, {module, function, length(args)}) || has_call_to?(args, module, mfas)
end
def has_call_to?(
%IR.RemoteFunctionCall{
module: %IR.AtomType{value: call_module},
function: function,
args: args
},
module,
mfas
) do
MapSet.member?(mfas, {call_module, function, length(args)}) ||
has_call_to?(args, module, mfas)
end
def has_call_to?(term, module, mfas) when is_list(term) do
Enum.any?(term, &has_call_to?(&1, module, mfas))
end
def has_call_to?(term, module, mfas) when is_map(term) do
term
|> Map.values()
|> has_call_to?(module, mfas)
end
def has_call_to?(term, module, mfas) when is_tuple(term) do
term
|> Tuple.to_list()
|> has_call_to?(module, mfas)
end
def has_call_to?(_term, _module, _mfas), do: false
defp build_function_capture_ir(function_capture, module, function) do
function_capture
|> Macro.escape()
|> Transformer.transform(%Context{})
|> Map.put(:captured_module, module)
|> Map.put(:captured_function, function)
end
end