defmodule Zig.Sema do
require EEx
alias Zig.Manifest
alias Zig.Type
alias Zig.Type.Function
alias Zig.Type.Manypointer
alias Zig.Type.Struct
@spec analyze_file!(module :: map, opts :: keyword) :: keyword
# updates the per-function options to include the semantically understood type
# information. Also strips "auto" from the nif information to provide a finalized
# keyword list of functions with their options.
def analyze_file!(%{functions: functions, types: types}, opts) do
Enum.each(functions, &validate_usable!(&1, types, opts))
# `nifs` option could either be {:auto, keyword} which means that the full
# list of functions should be derived from the semantic analysis, determining
# which functions have `pub` declaration, with certain functions optionally
# having their specification overloaded.
#
# it could also be just a list of functions with their specifications, in
# which case those are the *only* functions that will be included.
case Keyword.fetch!(opts, :nifs) do
{:auto, specified_fns} ->
default_options = Keyword.fetch!(opts, :default_options)
# make sure all of the specified functions are indeed found by sema.
Enum.each(specified_fns, fn
{specified_fn, _} ->
unless Enum.any?(functions, &(&1.name == specified_fn)) do
raise CompileError,
description:
"function #{specified_fn} not found in semantic analysis of functions."
end
end)
Enum.map(
functions,
fn function = %{name: name} ->
function_opts = Keyword.get(specified_fns, name, default_options)
adjusted_function = adjust_raw(function, function_opts)
{name, Keyword.put(function_opts, :type, adjusted_function)}
end
)
selected_fns when is_list(selected_fns) ->
Enum.map(selected_fns, fn
{selected_fn, function_opts} ->
function = Enum.find(functions, &(&1.name == selected_fn))
unless function do
raise CompileError,
description:
"function #{selected_fn} not found in semantic analysis of functions."
end
adjusted_function = adjust_raw(function, function_opts)
{selected_fn, Keyword.put(function_opts, :type, adjusted_function)}
end)
end
end
defp adjust_raw(function, opts) do
case {function, opts[:raw]} do
{_, nil} ->
function
# use this to identify that the function is a raw call. `child` could be
# either :term or :erl_nif_term.
{%{params: [:env, _, %Manypointer{child: child}]}, integer} when is_integer(integer) ->
%{function | params: List.duplicate(child, integer), arity: integer}
{_, {:c, integer}} when is_integer(integer) ->
%{function | params: List.duplicate(:erl_nif_term, integer), arity: integer}
end
end
def run_sema!(file, module \\ nil, opts \\ [include_dir: []]) do
case run_sema(file, module, opts) do
{:ok, sema} -> sema
end
rescue
e in Zig.CompileError ->
reraise Zig.CompileError.to_error(e, opts), __STACKTRACE__
end
def run_sema(file, module \\ nil, opts \\ []) do
opts = Keyword.put_new(opts, :include_dir, [])
# TODO: integrate error handling here, and make this a common nexus for
# file compilation
with {:ok, sema_str} <- Zig.Command.run_sema(file, opts),
{:ok, sema_json} <- Jason.decode(sema_str) do
if opts[:dump_sema] do
sema_json_pretty = Jason.encode!(sema_json, pretty: true)
IO.puts([IO.ANSI.yellow(), sema_json_pretty, IO.ANSI.reset()])
end
# filter out any functions present in "ignore" clause.
sema_json
|> Map.update!("functions", &filter_ignores(&1, opts))
|> module_to_types(module)
end
end
defp filter_ignores(functions, opts) do
ignores =
opts
|> Keyword.get(:ignore, [])
|> List.wrap()
|> Enum.map(&to_string/1)
Enum.reject(functions, &(&1["name"] in ignores))
end
defp module_to_types(%{"functions" => functions, "types" => types, "decls" => decls}, module) do
{:ok,
%{
functions: Enum.map(functions, &Function.from_json(&1, module)),
types: Enum.map(types, &type_from_json(&1, module)),
decls: Enum.map(decls, &const_from_json/1)
}}
end
defp type_from_json(%{"name" => name, "type" => type}, module) do
%{name: String.to_atom(name), type: Type.from_json(type, module)}
end
defp const_from_json(%{"name" => name, "type" => type}) do
%{name: String.to_atom(name), type: String.to_atom(type)}
end
defp validate_usable!(function, types, opts) do
with :ok <- validate_args(function, types),
:ok <- validate_return(function.return),
:ok <- validate_struct_pub(function.return, types, "return", function.name) do
:ok
else
{:error, msg} ->
file_path =
opts
|> Keyword.fetch!(:mod_file)
|> Path.relative_to_cwd()
%{location: {raw_line, _}} =
opts
|> Keyword.fetch!(:parsed)
|> find_function(function.name)
{file, line} =
opts
|> Keyword.fetch!(:manifest)
|> Manifest.resolve(file_path, raw_line)
raise CompileError,
description: msg,
file: file,
line: line
end
end
defp find_function(%{code: code}, function) do
Enum.find(code, fn
%Zig.Parser.Function{name: ^function} -> true
%Zig.Parser.Const{name: ^function} -> true
_ -> false
end)
end
defp validate_args(function, types) do
function.params
|> Enum.with_index(1)
|> Enum.reduce_while(:ok, fn
{type, index}, :ok ->
case validate_struct_pub(type, types, "argument #{index}", function.name) do
:ok ->
{:cont, :ok}
error = {:error, _msg} ->
{:halt, error}
end
end)
end
# explicitly, all return values that are permitted
defp validate_return(type) do
if Type.return_allowed?(type) do
:ok
else
{:error, "functions returning #{type} cannot be nifs"}
end
end
defp validate_struct_pub(%Struct{name: name}, types, location, function) do
Enum.reduce_while(
types,
{:error, "struct `#{name}` (#{location} of function `#{function}`) is not a `pub` struct"},
fn
%{name: type_name}, error = {:error, _} ->
if Atom.to_string(type_name) == name do
{:halt, :ok}
else
{:cont, error}
end
end
)
end
defp validate_struct_pub(_, _, _, _), do: :ok
end