Skip to content

Commit

Permalink
gets typespecs working
Browse files Browse the repository at this point in the history
  • Loading branch information
ityonemo committed May 5, 2024
1 parent 217bc35 commit 1c99662
Show file tree
Hide file tree
Showing 38 changed files with 660 additions and 727 deletions.
2 changes: 1 addition & 1 deletion guides/3-allocators.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ to avoid cache misses in your execution thread.
const beam = @import("beam");
pub fn allocate_raw(count: usize) !beam.term {
var slice = try beam.allocator.alloc(u16, count);
const slice = try beam.allocator.alloc(u16, count);
defer beam.allocator.free(slice);
for (slice, 0..) |*entry, index| {
Expand Down
8 changes: 4 additions & 4 deletions guides/6-c_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Zigler offers several tools to integrate your code with C and C++ code.
If you want to compile C or C++ files using the C and C++ toolchain bundled
with the zig programming language, you should include a path to your
include directory and a source path or list of source paths to be compiled,
via the `include_dir` and `c_src` module options.
via the `include_dir` and `src` module options.

> ### source paths {: .info }
>
Expand All @@ -34,7 +34,7 @@ defmodule CompilingC do
use Zig,
otp_app: :zigler,
include_dir: "include",
c_src: "src/*"
src: "src/*"

~Z"""
const c = @cImport(@cInclude("included.h"));
Expand Down Expand Up @@ -92,7 +92,7 @@ if {:unix, :linux} == :os.type() do

It's also possible to also automatically create nifs without writing zig
function shims. This works either with linking an external library with
`link_lib` or building your own code with `c_src`.
`link_lib` or building your own code with `src`.

Because the C ABI exposes all functions publically in a global namespace,
we can't use zigler's automatic detection to decide which functions to
Expand All @@ -102,7 +102,7 @@ Here are the steps to using `easy_c`:

- declare which header file you'd like to use with the `easy_c` module option
- you may want to add `include_dir` if the header isn't a system C header.
- add a `link_lib` or `c_src` option to make sure that the functions are built.
- add a `link_lib` or `src` option to make sure that the functions are built.
- declare which functions you'd like to hoist into the module.

In this example we'll use the `cblas_daxpy` function, which takes a length,
Expand Down
2 changes: 1 addition & 1 deletion lib/zig.ex
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ defmodule Zig do
otp_app: :my_app,
link_libcpp: true, # note: optional for c-only code
include_dir: ["include"],
c_src: [
src: [
"some_c_source.c",
{"some_cpp_source.cpp", ["-std=c++17"]},
{"directory_of_files/*", ["-std=c99"]},
Expand Down
34 changes: 34 additions & 0 deletions lib/zig/_c.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Zig.C do
@moduledoc false

# module creates a struct that defines the options for
# c-interoperability with zigler

defstruct include_dirs: [],
src: [],
link_lib: [],
link_libcpp: false

@type t :: %__MODULE__{
include_dirs: [Path.t()],
link_lib: [Path.t()],
link_libcpp: boolean,
src: src_opts()
}

@type opts :: [
include_dirs: Path.t() | [Path.t()],
link_lib: Path.t() | [Path.t()],
link_libcpp: boolean,
src: src_opts()
]

@type src_opts :: term

def new(opts) do
struct!(__MODULE__,
include_dirs: List.wrap(opts[:include_dirs]),
link_lib: List.wrap(opts[:link_lib])
)
end
end
82 changes: 32 additions & 50 deletions lib/zig/module.ex → lib/zig/_module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ defmodule Zig.Module do
module
"""

# TODO: write out a struct that will hold the nif opts for the module.

alias Zig.C
alias Zig.Nif
alias Zig.Resources
alias Zig.Type.Error

# for easy access in EEx files
@behaviour Access
Expand All @@ -32,6 +32,7 @@ defmodule Zig.Module do
:sema,
:parsed,
:version,
:c,
language: Elixir,
nifs: {:auto, []},
ignore: [],
Expand All @@ -40,10 +41,6 @@ defmodule Zig.Module do
dump: false,
dump_sema: false,
dump_build_zig: false,
include_dir: [],
link_lib: [],
link_libcpp: false,
c_src: [],
callbacks: [],
default_nif_opts: []
]
Expand All @@ -63,8 +60,9 @@ defmodule Zig.Module do
sema: Sema.t(),
parsed: Parser.t(),
version: String.t(),
c: C.opts() | C.t(),
language: Elixir | :erlang,
nifs: nif_opts() | [Nif.t()],
nifs: Nif.opts() | [Nif.t()],
ignore: [atom()],
packages: [packagespec()],
resources: [atom()],
Expand All @@ -73,36 +71,17 @@ defmodule Zig.Module do
dump_build_zig: boolean | :stdout | :stderr | Path.t(),
build_zig: nil | Path.t(),
precompiled: nil | precompiledspec(),
include_dir: [],
link_lib: [],
link_libcpp: false,
c_src: [],
c: C.t(),
callbacks: callback_opts(),
default_nif_opts: default_nif_opts()
default_nif_opts: [Nif.defaultable_opts()]
}

@type packagespec() :: {name :: atom(), {path :: Path.t(), deps :: [atom]}}
# NB: this is going to become more complex for security reasons.
@type precompiledspec() :: Path.t()
@type callback_opts() :: [on_load: atom(), on_upgrade: atom(), on_unload: atom()]
@type nif_opts() :: [
cleanup: boolean,
leak_check: boolean,
ignore: boolean,
export: boolean,
concurrency: Zig.Nif.Concurrency.concurrency(),
raw: boolean(),
args: %{optional(integer) => arg_opts()},
return: :list | :binary,
alias: atom(),
doc: String.t(),
spec: Macro.t()
]
@type arg_opts() :: [{atom, term}]

@type default_nif_opts() :: [cleanup: boolean, leak_check: boolean]

@nif_opts ~w[cleanup leak_check ignore]a

@defaultable_nif_opts ~w[cleanup leak_check]a

def new(opts, caller) do
# make sure that the caller has declared otp_app here.
Expand All @@ -124,9 +103,9 @@ defmodule Zig.Module do
end

opts
|> Keyword.drop(@nif_opts)
|> Keyword.drop(@defaultable_nif_opts)
|> Keyword.merge(
default_nif_opts: Keyword.take(opts, @nif_opts),
default_nif_opts: Keyword.take(opts, @defaultable_nif_opts),
module: caller.module,
file: caller.file
)
Expand All @@ -137,12 +116,15 @@ defmodule Zig.Module do
defp normalize_options(opts) do
opts
|> obtain_version
|> Keyword.update(:c, nil, &C.new/1)

# |> normalize_include_dirs
# |> normalize_src
# |> normalize_nifs
# |> normalize_libs
# |> normalize_build_opts
# |> normalize_include_dirs
# |> normalize_c_src
# |> normalize_src
# |> EasyC.normalize_aliasing()
end

Expand All @@ -169,16 +151,6 @@ defmodule Zig.Module do
end)
end

# defp normalize_nifs(opts) do
# Keyword.update!(opts, :nifs, fn
# {:auto, opts} ->
# {:auto, Enum.map(opts, &Nif.normalize_options!(&1, common_options))}

# opts ->
# Enum.map(opts, &Nif.normalize_options!(&1, common_options))
# end)
# end

# internal helpers
defp table_entries(nifs) when is_list(nifs) do
nifs
Expand All @@ -204,11 +176,10 @@ defmodule Zig.Module do
EEx.function_from_file(:def, :render_zig, nif, [:assigns])

def render_elixir(module, zig_code) do
nif_name = "#{module.module}"
module_name = "#{module.module}"

load_nif_fn =
quote do
@zigler_module unquote(Macro.escape(module))
def __load_nifs__ do
# LOADS the nifs from :code.lib_dir() <> "ebin", which is
# a path that has files correctly moved in to release packages.
Expand All @@ -217,27 +188,38 @@ defmodule Zig.Module do
unquote(module.otp_app)
|> :code.priv_dir()
|> Path.join("lib")
|> Path.join(unquote(nif_name))
|> Path.join(unquote(module_name))
|> String.to_charlist()
|> :erlang.load_nif(0)
|> case do
:ok ->
Logger.debug("loaded module at #{unquote(nif_name)}")
Logger.debug("loaded module at #{unquote(module_name)}")

error = {:error, any} ->
Logger.error("loading module #{unquote(nif_name)} #{inspect(any)}")
Logger.error("loading module #{unquote(module_name)} #{inspect(any)}")
end
end
end

function_code = Enum.map(module.nifs, &Nif.render_elixir/1)

# TODO: there might be a smarter way of getting this.
manifest_code =
if Enum.any?(module.nifs, &match?(%Error{}, &1.return.type)) do
quote do
require Zig.Manifest
Zig.Manifest.resolver(unquote(module.manifest), unquote(module.module_code_path), :defp)
end
end

quote do
# these two attribs can be persisted for code inspection.
@zigler_module unquote(Macro.escape(module))
@zig_code unquote(zig_code)

unquote_splicing(function_code)
unquote(load_nif_fn)
require Zig.Manifest
Zig.Manifest.resolver(unquote(module.manifest), unquote(module.module_code_path), :defp)
unquote(manifest_code)

def _format_error(_, [{_, _, _, opts} | _rest] = _stacktrace) do
if formatted = opts[:zigler_error], do: formatted, else: %{}
Expand Down
49 changes: 34 additions & 15 deletions lib/zig/nif.ex → lib/zig/_nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ defmodule Zig.Nif do

defstruct @enforce_keys ++ ~w[line signature params return leak_check alias doc spec]a

alias Zig.Nif.Concurrencny
alias Zig.Nif.DirtyCpu
alias Zig.Nif.DirtyIo
alias Zig.Nif.Synchronous
alias Zig.Nif.Threaded
alias Zig.Nif.Yielding
alias Zig.Parameter
alias Zig.Return
alias Zig.Type
alias Zig.Type.Error
alias Zig.Type.Function

@type t :: %__MODULE__{
name: atom,
export: boolean,
concurrency: Synchronous | Threaded | Yielding | DirtyCpu | DirtyIo,
concurrency: Concurrency.t(),
line: integer,
file: Path.t(),
signature: Function.t(),
Expand All @@ -40,6 +42,22 @@ defmodule Zig.Nif do
spec: Macro.t()
}

@type defaultable_opts ::
{:cleanup, boolean}
| {:leak_check, boolean}

@type individual_opts ::
{:ignore, boolean}
| {:export, boolean}
| {:concurrency, Concurrency.t()}
| {:args, %{optional(integer) => Parameter.opts()}}
| {:return, Return.opts()}
| {:alias, atom()}
| {:doc, String.t()}
| {:spec, Macro.t()}

@type opts() :: [defaultable_opts | individual_opts]

@impl true
defdelegate fetch(function, key), to: Map

Expand All @@ -63,15 +81,6 @@ defmodule Zig.Nif do
}
end

# defp extract_raw(raw_opt, %{return: return}) do
# case {raw_opt, return} do
# {nil, _} -> nil
# {{:c, arity}, _} when is_integer(arity) -> :c
# {arity, :term} when is_integer(arity) -> :beam
# {arity, :erl_nif_term} when is_integer(arity) -> :erl_nif
# end
# end

def render_elixir(%{concurrency: concurrency} = nif) do
doc =
if nif_doc = nif.doc do
Expand All @@ -81,18 +90,15 @@ defmodule Zig.Nif do
end

typespec =
case nif.spec do
case nif.spec |> dbg do
false ->
quote do
end

_ ->
quote do
@spec unquote(render_elixir_spec(nif))
end

# quote do
# @spec unquote(Function.render_elixir_spec(nif.spec, nif.name))
# end
end

functions = concurrency.render_elixir(nif)
Expand All @@ -104,6 +110,19 @@ defmodule Zig.Nif do
end
end

def render_elixir_spec(nif) do
param_spec =
nif.params
|> Enum.sort()
|> Enum.map(fn {_, p} -> Type.render_elixir_spec(p.type, :param, p) end)

return_spec = Type.render_elixir_spec(nif.return.type, :return, nif.return)

quote context: Elixir do
unquote(nif.name)(unquote_splicing(param_spec)) :: unquote(return_spec)
end
end

def render_erlang(nif, _opts \\ []) do
# TODO: typespec in erlang.

Expand Down
Loading

0 comments on commit 1c99662

Please sign in to comment.