defmodule Zig.Sema do @moduledoc false require EEx alias Zig.Attributes alias Zig.Module alias Zig.Nif alias Zig.Parameter alias Zig.Return alias Zig.Type alias Zig.Type.Error alias Zig.Type.Function alias Zig.Type.Integer alias Zig.Type.Manypointer alias Zig.Type.Pointer @enforce_keys [:functions, :types, :decls, :callbacks] defstruct @enforce_keys @type t :: %__MODULE__{ functions: [Function.t()], types: keyword(Type.t()), decls: keyword(Type.t()), callbacks: [Function.t()] } # PHASE 1: SEMA EXECUTION @spec run_sema!(Module.t()) :: Module.t() # performs the first stage of semantic analysis: # actually executing the zig command to obtain the semantic analysis of the # desired file. def run_sema!(module) do module.zig_code_path |> Zig.Command.run_sema!(attribs_file: Attributes.code_path(module), c: module.c) |> Jason.decode!() |> tap(&maybe_dump(&1, module)) |> reject_ignored(module) |> reject_allocators(module) |> reject_error_interpreters(module) |> assign_callbacks(module) |> integrate_sema(module) |> then(&Map.replace!(module, :sema, &1)) rescue e in Zig.CompileError -> reraise Zig.CompileError.resolve(e, module), __STACKTRACE__ end def run_sema_doc(file) do file |> Zig.Command.run_sema_doc!() |> Jason.decode!() |> integrate_sema(%{module: nil}) end defp assign_callbacks(sema, module) do sema |> Map.put("callbacks", []) |> then(fn sema -> Enum.reduce(module.callbacks, sema, &move_callback(&1, &2, module)) end) end defp move_callback( {type, name}, %{"functions" => functions, "callbacks" => callbacks} = sema, module ) do {new_functions, new_callback} = find_remove(functions, [], "#{name}", type, module) %{sema | "functions" => new_functions, "callbacks" => [{type, new_callback} | callbacks]} end defp find_remove([%{"name" => name} = callback | rest], so_far, name, _type, _module), do: {Enum.reverse(so_far, rest), callback} defp find_remove([other | rest], so_far, name, type, module), do: find_remove(rest, [other | so_far], name, type, module) defp find_remove([], so_far, _name, _type, _module), do: {so_far, nil} # removes "ignored", "allocators" and "error_interpreter" functions from the semantic analysis. defp reject_ignored(json, module) do ignored = Enum.map(module.ignore, &"#{&1}") Map.update!(json, "functions", fn functions -> Enum.reject(functions, &(&1["name"] in ignored)) end) end defp reject_allocators(json, module) do nifs = case module.nifs do {:auto, list} -> list list -> list end allocators = nifs |> Enum.flat_map(fn {_name, opts} -> List.wrap(opts[:allocator]) end) |> Enum.map(&"#{&1}") Map.update!(json, "functions", fn functions -> Enum.reject(functions, &(&1["name"] in allocators)) end) end defp reject_error_interpreters(json, module) do nifs = case module.nifs do {:auto, list} -> list list -> list end # try to find a list of all functions that are opts >> return >> error # but then also axe out any functions that are also explicitly specified in the nifs list # then convert into a list of strings error_functions = nifs |> Enum.flat_map(fn {_, opts} -> error_fun(opts) end) |> Enum.reject(&Keyword.has_key?(nifs, &1)) |> Enum.map(&"#{&1}") Map.update!(json, "functions", fn functions -> Enum.reject(functions, &(&1["name"] in error_functions)) end) end defp error_fun(opts) do opts |> Keyword.get(:return, []) |> List.wrap() |> Enum.find_value(fn {:error, fun} -> fun _ -> nil end) |> List.wrap() end defp integrate_sema( %{ "functions" => functions, "types" => types, "decls" => decls } = sema, module ) do callbacks = List.wrap(sema["callbacks"]) %__MODULE__{ functions: Enum.map(functions, &Function.from_json(&1, module.module)), types: Enum.map(types, &type_from_json(&1, module.module)), decls: Enum.map(decls, &const_from_json/1), callbacks: Enum.map(callbacks, fn {type, json} -> {type, json && Function.from_json(json, module.module)} end) } 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 maybe_dump(sema_json, module) do if module.dump_sema do sema_json_pretty = Jason.encode!(sema_json, pretty: true) IO.puts([IO.ANSI.yellow(), sema_json_pretty, IO.ANSI.reset()]) end end @spec analyze_file!(Module.t()) :: Module.t() # 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!(%{sema: %{functions: functions, types: _types}} = module) do # `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. # check for invalid callbacks check_invalid_callbacks!(module) nifs = case module.nifs do {:auto, specified_fns} -> # make sure that all of the specified functions exist in sema. Enum.each(specified_fns, fn {name, nif_opts} -> expected_name = Keyword.get(nif_opts, :alias, name) unless Enum.any?(functions, &(&1.name == expected_name)) do needed_msg = if nif_opts[:alias], do: " (needed by nif #{name})" raise CompileError, description: "public function named `#{expected_name}`#{needed_msg} not found in semantic analysis of module.", file: module.file, line: module.line end end) Enum.map(functions, fn function -> nif_opts = Keyword.get(specified_fns, function.name, module.default_nif_opts) function.name |> Nif.new(module, nif_opts) |> Nif.set_file_line(module.manifest_module, module.parsed) |> apply_from_sema(function, nif_opts) end) ++ Enum.flat_map(specified_fns, fn {name, nif_opts} -> List.wrap( if expected_name = Keyword.get(nif_opts, :alias) do function = Enum.find(functions, &(&1.name == expected_name)) # TODO: abstract this with below name |> Nif.new(module, nif_opts) |> Nif.set_file_line(module.manifest_module, module.parsed) |> apply_from_sema(function, nif_opts) end ) end) selected_fns when is_list(selected_fns) -> Enum.map(selected_fns, fn {name, nif_opts} -> expected_name = Keyword.get(nif_opts, :alias, name) if function = Enum.find(functions, &(&1.name == expected_name)) do # TODO: abstract this with above name |> Nif.new(module, nif_opts) |> Nif.set_file_line(module.manifest_module, module.parsed) |> apply_from_sema(function, nif_opts) else needed_msg = if nif_opts[:alias], do: " (needed by nif #{name})" raise CompileError, description: "public function named `#{expected_name}`#{needed_msg} not found in semantic analysis of module.", file: module.file, line: module.line end end) end Enum.each(nifs, &validate_nif!(&1)) %{module | nifs: nifs} end defp apply_from_sema( nif, %Function{ arity: 3, params: [:env, %Integer{}, %Manypointer{child: t}], return: t } = sema, opts ) when t in ~w[term erl_nif_term]a do arities = case Keyword.fetch(opts, :arity) do {:ok, arity} when arity in 0..63 -> arities(arity) {:ok, {:.., _, _} = range} -> arities(range) {:ok, list} when is_list(list) -> Enum.flat_map(list, &arities/1) :error -> raise CompileError, description: "the raw function #{inspect(nif.module)}.#{nif.name}/? must have arities specified in zigler parameters", file: nif.file, line: nif.line end %{nif | signature: sema, raw: t, params: arities, return: Return.new(t)} end defp apply_from_sema(%{params: nif_params!} = nif, sema, opts) do nif_params! = nif_params! || %{} Enum.each(nif_params!, fn {index, _} -> if index >= sema.arity do raise CompileError, description: "nif function `#{nif.name}` has an arity of #{sema.arity}, but parameter #{index} was specified", file: nif.file, line: nif.line end end) %{ nif | signature: sema, params: params_from_sema(sema, nif_params!), return: return_from_sema(sema, opts) } end defp arities(integer) when is_integer(integer), do: [integer] defp arities({:.., _, [start, finish]}), do: Enum.to_list(start..finish) defp params_from_sema(%{params: params}, opts) do params |> Enum.with_index() |> Enum.reduce(%{}, fn {param, index}, so_far -> Map.put(so_far, index, Parameter.new(param, List.wrap(opts[index]))) end) end defp return_from_sema(%{return: return}, opts) do opts |> Keyword.get(:return, []) |> add_in_out_param(opts[:params] || %{}) |> then(&Return.new(return, &1)) end @in_out_styles [:in_out, in_out: true] defp add_in_out_param(return_opts, params) do params |> Enum.flat_map(fn {index, param_opts} -> if Enum.any?(List.wrap(param_opts), &(&1 in @in_out_styles)) do [index] else [] end end) |> case do [] -> return_opts [int] -> Keyword.put(return_opts, :in_out, "arg#{int}") [_ | _] -> raise CompileError, "can't have more than one in-out parameter" end end defp validate_nif!(%{raw: nil} = nif) do Enum.each(nif.params, &validate_param!(&1, nif)) validate_return!(nif) end defp validate_nif!(_raw_nif), do: :ok defp validate_param!({_, %{in_out: true} = param}, nif) do unless Type.in_out_allowed?(param.type) do raise CompileError, description: "nif function `#{nif.name}` cannot have a an in-out parameter of type #{Type.render_zig(param.type)}", file: nif.file, line: nif.line end end defp validate_param!({_, param}, nif) do unless Type.get_allowed?(param.type) do raise CompileError, description: "nif function `#{nif.name}` cannot have a value of type #{Type.render_zig(param.type)} as a parameter", file: nif.file, line: nif.line end end defp validate_return!(nif) do unless Type.make_allowed?(nif.return.type) do raise CompileError, description: "nif function `#{nif.name}` cannot return a value of type #{Type.render_zig(nif.return.type)}", file: nif.file, line: nif.line end end defp check_invalid_callbacks!(module) do Enum.each(module.sema.callbacks, fn {type, nil} -> seek_and_raise!( type, module, &"#{type} callback #{&1}must be declared `pub`", &"#{type} callback #{&1}not found" ) {:on_load, %{arity: arity}} when arity not in [2, 3] -> seek_and_raise!(:on_load, module, &"on_load callback #{&1}must have arity 2 or 3") {:on_load, %{arity: 2} = function} -> case function.params do [%Pointer{optional: true, child: %Pointer{optional: true}}, second] -> if Type.get_allowed?(second) do :ok else seek_and_raise!( :on_load, module, &"on_load (automatic-style) callback #{&1}must have a second parameter of a type compatible with `beam.get`.\n\n got: `#{Type.render_zig(second)}`" ) end [first, _] -> seek_and_raise!( :on_load, module, &"on_load (automatic-style) callback #{&1}must have a first paramater of a `?*?*` type.\n\n got: `#{Type.render_zig(first)}`" ) end case function.return do :void -> :ok %Integer{} -> :ok %Zig.Type.Enum{} -> :ok %Error{child: :void} -> :ok bad -> seek_and_raise!( :on_load, module, &"on_load (automatic-style) callback #{&1}must have a return of type integer, enum, `void`, or `!void`.\n\n got: `#{Type.render_zig(bad)}`" ) end {:on_load, %{arity: 3} = function} -> case function.params do [:env, %Pointer{optional: true, child: %Pointer{optional: true}}, :erl_nif_term] -> :ok [first, _, _] when first != :env -> seek_and_raise!( :on_load, module, &"on_load (raw-style) callback #{&1}must have a first parameter of type `beam.env`.\n\n got: `#{Type.render_zig(first)}`" ) [_, _, third] when third != :erl_nif_term -> seek_and_raise!( :on_load, module, &"on_load (raw-style) callback #{&1}must have a third parameter of type `e.ErlNifTerm`.\n\n got: `#{Type.render_zig(third)}`" ) [_, second, _] -> seek_and_raise!( :on_load, module, &"on_load (raw-style) callback #{&1}must have a second parameter of type `?*?*`.\n\n got: `#{Type.render_zig(second)}`" ) end case function.return do %Integer{signedness: :signed, bits: 32} -> :ok bad -> seek_and_raise!( :on_load, module, &"on_load (raw-style) callback #{&1}must have return type `c_int`.\n\n got: `#{Type.render_zig(bad)}`" ) end {:on_upgrade, %{arity: arity}} when arity not in [3, 4] -> seek_and_raise!(:on_upgrade, module, &"on_upgrade callback #{&1}must have arity 3 or 4") {:on_upgrade, %{arity: 3} = function} -> case function.params do [ %Pointer{optional: true, child: %Pointer{optional: true}}, %Pointer{optional: true, child: %Pointer{optional: true}}, third ] -> if Type.get_allowed?(third) do :ok else seek_and_raise!( :on_upgrade, module, &"on_upgrade (automatic-style) callback #{&1}must have a third parameter of a type compatible with `beam.get`.\n\n got: `#{Type.render_zig(third)}`" ) end [%Pointer{optional: true, child: %Pointer{optional: true}}, second, _] -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (automatic-style) callback #{&1}must have a second parameter of type `?*?*`.\n\n got: `#{Type.render_zig(second)}`" ) [first, _, _] -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (automatic-style) callback #{&1}must have a first parameter of type `?*?*`.\n\n got: `#{Type.render_zig(first)}`" ) end case function.return do :void -> :ok %Integer{} -> :ok %Zig.Type.Enum{} -> :ok %Error{child: :void} -> :ok bad -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (automatic-style) callback #{&1}must have an integer, enum, `void`, or `!void` as a return.\n\n got: `#{Type.render_zig(bad)}`" ) end {:on_upgrade, %{arity: 4} = function} -> case function.params do [ :env, %Pointer{optional: true, child: %Pointer{optional: true}}, %Pointer{optional: true, child: %Pointer{optional: true}}, :erl_nif_term ] -> :ok [ :env, %Pointer{optional: true, child: %Pointer{optional: true}}, %Pointer{optional: true, child: %Pointer{optional: true}}, fourth ] -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (raw-style) callback #{&1}must have a fourth parameter of type `e.ErlNifTerm`.\n\n got: `#{Type.render_zig(fourth)}`" ) [ :env, %Pointer{optional: true, child: %Pointer{optional: true}}, third, _ ] -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (raw-style) callback #{&1}must have a third parameter of type `?*?*`.\n\n got: `#{Type.render_zig(third)}`" ) [ :env, second, _, _ ] -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (raw-style) callback #{&1}must have a second parameter of type `?*?*`.\n\n got: `#{Type.render_zig(second)}`" ) [first, _, _, _] -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (raw-style) callback #{&1}must have a first parameter of type `beam.env`.\n\n got: `#{Type.render_zig(first)}`" ) end case function.return do %Integer{signedness: :signed, bits: 32} -> :ok bad -> seek_and_raise!( :on_upgrade, module, &"on_upgrade (raw-style) callback #{&1}must have an `c_int` as a return.\n\n got: `#{Type.render_zig(bad)}`" ) end {:on_unload, %{arity: arity}} when arity not in [1, 2] -> seek_and_raise!(:on_unload, module, &"on_unload callback #{&1}must have arity 1 or 2") {:on_unload, %{arity: 1} = function} -> case function.params do [%Pointer{optional: true}] -> :ok [first] -> seek_and_raise!( :on_unload, module, &"on_unload (automatic-style) callback #{&1}must have a parameter of type `?*`.\n\n got: `#{Type.render_zig(first)}`" ) end case function.return do :void -> :ok bad -> seek_and_raise!( :on_unload, module, &"on_unload (automatic-style) callback #{&1}must have `void` as a return.\n\n got: `#{Type.render_zig(bad)}`" ) end {:on_unload, %{arity: 2} = function} -> case function.params do [:env, %Pointer{optional: true}] -> :ok [first, %Pointer{optional: true}] -> seek_and_raise!( :on_unload, module, &"on_unload (raw-style) callback #{&1}must have a first parameter of type `beam.env`.\n\n got: `#{Type.render_zig(first)}`" ) [_, second] -> seek_and_raise!( :on_unload, module, &"on_unload (raw-style) callback #{&1}must have a second parameter of type `?*`.\n\n got: `#{Type.render_zig(second)}`" ) end case function.return do :void -> :ok bad -> seek_and_raise!( :on_unload, module, &"on_unload (raw-style) callback #{&1}must have `void` as a return.\n\n got: `#{Type.render_zig(bad)}`" ) end _ -> :ok end) end defp seek_and_raise!(type, module, error1, error2 \\ nil) do name = module.callbacks[type] print_name = unless type == name, do: "#{name} " for %{name: ^name, location: {line, _col}} <- module.parsed.code do {file, line} = module.manifest_module.__resolve(%{file_name: module.zig_code_path, line: line}) raise CompileError, description: error1.(print_name), file: file, line: line end raise CompileError, description: if(error2, do: error2.(print_name), else: error1.(print_name)), file: module.file, line: module.line end end