Skip to content

Commit

Permalink
Support private defdp functions with proper 'unused' warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
arnodirlam committed Oct 26, 2024
1 parent 883dfc7 commit a5103e7
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 14 deletions.
2 changes: 2 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
locals_without_parens = [
defd: 1,
defd: 2,
defdp: 1,
defdp: 2,
field_group: 1,
import_rules: 1,
infer: 1,
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- Support `defdp`: Private `defd` functions with proper 'unused' warnings
- `case`: Support carets, assigns and data loading in map keys
- `fn`: Support multiple clauses

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ you have to keep that in mind yourself.

### Syntax

All syntax except `for` and `with` is supported in `defd` functions.
All syntax except `for` and `with` is supported in `defd` and (private) `defdp` functions.

### Standard library

Expand Down
39 changes: 39 additions & 0 deletions lib/dx/defd.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Dx.Defd do
alias Dx.Defd.Ast
alias Dx.Defd.Util
alias Dx.Evaluation, as: Eval

Expand All @@ -12,6 +13,7 @@ defmodule Dx.Defd do
unquote(defd_call)
end)
end
|> mark_use(call)
end

defmacro load!(call, opts \\ []) do
Expand All @@ -22,6 +24,7 @@ defmodule Dx.Defd do
unquote(defd_call)
end)
end
|> mark_use(call)
end

defmacro get(call, opts \\ []) do
Expand All @@ -32,13 +35,15 @@ defmodule Dx.Defd do

unquote(defd_call)
end
|> mark_use(call)
end

defmacro get!(call, opts \\ []) do
quote do
Dx.Defd.get(unquote(call), unquote(opts))
|> Dx.Result.unwrap!()
end
|> mark_use(call)
end

defp call_to_defd({:|>, _meta, _pipeline} = ast, env) do
Expand All @@ -61,6 +66,32 @@ defmodule Dx.Defd do
{defd_name, meta, args}
end

defp mark_use(ast, call) do
case call_to_use(call, __ENV__) do
{name, arity} ->
Ast.block([
Ast.local_fun_ref(name, arity),
ast
])

_ ->
ast
end
end

defp call_to_use({:|>, _meta, _pipeline} = ast, env) do
ast
|> Macro.expand_once(env)
|> call_to_use(env)
end

defp call_to_use(call, _env) do
case Macro.decompose_call(call) do
{name, args} -> {name, length(args)}
_ -> nil
end
end

defmacro defd(call) do
define_defd(:def, call, __CALLER__)
end
Expand All @@ -69,6 +100,14 @@ defmodule Dx.Defd do
define_defd(:def, call, block, __CALLER__)
end

defmacro defdp(call) do
define_defd(:defp, call, __CALLER__)
end

defmacro defdp(call, do: block) do
define_defd(:defp, call, block, __CALLER__)
end

@doc """
Used to wrap calls to non-Dx defined functions.
It doesn't run any code, but makes these calls explicit and mutes Dx compiler warnings.
Expand Down
10 changes: 10 additions & 0 deletions lib/dx/defd/ast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ defmodule Dx.Defd.Ast do

import Dx.Defd.Ast.Guards

def local_fun_ref({name, arity}), do: local_fun_ref(name, arity)

def local_fun_ref(name, meta \\ [], arity, meta2 \\ []) do
{:&, meta, [{:/, [], [{name, meta2, nil}, arity]}]}
end

def block(lines \\ [], meta \\ []) do
{:__block__, meta, lines}
end

def is_function(
{:ok,
{:%, _,
Expand Down
77 changes: 64 additions & 13 deletions lib/dx/defd/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ defmodule Dx.Defd.Compiler do
@doc false
def __compile__(%Macro.Env{module: module, file: file, line: line}, exports, eval_var) do
defds = compile_prepare_arities(exports)
all_arities = all_arities(exports)

state = %{
module: module,
file: file,
line: line,
function: nil,
defds: defds,
all_arities: all_arities,
used_defds: MapSet.new(),
args: MapSet.new(),
var_index: 1,
scope_args: [],
Expand All @@ -39,9 +42,42 @@ defmodule Dx.Defd.Compiler do
rewrite_underscore?: false
}

quoted = Enum.flat_map(exports, &compile_each_defd(&1, state))
{quoted, state} =
Enum.flat_map_reduce(exports, state, fn def, state ->
{definitions, new_state} = compile_each_defd(def, state)

{:__block__, [], quoted}
state = %{state | used_defds: new_state.used_defds}

{definitions, state}
end)

generated_functions =
Enum.flat_map(defds, fn {name, arity} ->
[
{Util.defd_name(name), arity + 1},
{Util.final_args_name(name), arity + 1},
{Util.scope_name(name), arity}
]
end)

suppress_unused_warnings_ast =
case MapSet.to_list(state.used_defds) ++ generated_functions do
[] ->
[]

defs ->
ast = Enum.map(defs, &Ast.local_fun_ref/1)

[
quote do
def unquote(:"__dx:suppress_unused_warnings__")() do
unquote(ast)
end
end
]
end

Ast.block(suppress_unused_warnings_ast ++ quoted)
end

defp compile_prepare_arities(definitions) do
Expand All @@ -51,6 +87,12 @@ defmodule Dx.Defd.Compiler do
do: {name, arity}
end

defp all_arities(definitions) do
Map.new(definitions, fn {{_name, arity} = def, %{defaults: defaults}} ->
{def, (arity - map_size(defaults))..arity}
end)
end

defp compile_each_defd({{name, arity} = def, def_meta}, state) do
%{defaults: defaults, opts: opts} = def_meta
debug_flags = List.wrap(opts[:debug])
Expand All @@ -75,6 +117,14 @@ defmodule Dx.Defd.Compiler do
other
end

all_args_with_defaults =
Enum.with_index(all_args, fn arg, i ->
case defaults do
%{^i => {meta, default}} -> {:\\, meta, [arg, default]}
%{} -> arg
end
end)

scope_args =
Enum.with_index(scope_args, fn arg, i ->
case defaults do
Expand Down Expand Up @@ -105,10 +155,11 @@ defmodule Dx.Defd.Compiler do
entrypoints =
case Keyword.get(opts, :def, :warn) do
:warn ->
Module.delete_definition(state.module, def)
for arity <- state.all_arities[def],
do: Module.delete_definition(state.module, {name, arity})

quote line: state.line do
Kernel.unquote(kind)(unquote(name)(unquote_splicing(all_args))) do
unquote(kind)(unquote(name)(unquote_splicing(all_args_with_defaults))) do
IO.warn("""
Use Dx.Defd.load as entrypoint.
""")
Expand All @@ -120,20 +171,17 @@ defmodule Dx.Defd.Compiler do
|> List.wrap()

:no_warn ->
Module.delete_definition(state.module, def)
for arity <- state.all_arities[def],
do: Module.delete_definition(state.module, {name, arity})

quote line: state.line do
Kernel.unquote(kind)(unquote(name)(unquote_splicing(all_args))) do
unquote(kind)(unquote(name)(unquote_splicing(all_args_with_defaults))) do
Dx.Defd.load!(unquote(name)(unquote_splicing(all_args)))
end
end
|> strip_definition_context()
|> List.wrap()

false ->
Module.delete_definition(state.module, def)
[]

:original ->
[]

Expand All @@ -157,9 +205,9 @@ defmodule Dx.Defd.Compiler do
end
end

if Enum.any?([:compiled_scope, :all], &(&1 in debug_flags)), do: Ast.p(scope)
definitions = entrypoints ++ [defd, final_args, scope]

entrypoints ++ [defd, final_args, scope]
{definitions, state}
end

defp append_arg({:when, meta, [args | guards]}, arg),
Expand Down Expand Up @@ -194,7 +242,6 @@ defmodule Dx.Defd.Compiler do

defp get_and_normalize_defd_and_scope({name, arity} = def, state) do
{:v1, kind, meta, clauses} = Module.get_definition(state.module, def)
# |> IO.inspect(label: "ORIG #{name}/#{arity}\n")

state = %{state | function: def, line: meta[:line] || state.line, rewrite_underscore?: true}

Expand Down Expand Up @@ -477,6 +524,8 @@ defmodule Dx.Defd.Compiler do
final_args_name = Util.final_args_name(fun_name)
scope_name = Util.scope_name(fun_name)

state = Map.update!(state, :used_defds, &MapSet.put(&1, {fun_name, arity}))

{:ok,
{:%, [line: line],
[
Expand Down Expand Up @@ -590,6 +639,8 @@ defmodule Dx.Defd.Compiler do
{fun_name, arity} in state.defds ->
defd_name = Util.defd_name(fun_name)

state = Map.update!(state, :used_defds, &MapSet.put(&1, {fun_name, arity}))

normalize_call_args(args, state, fn args ->
{defd_name, meta, args ++ [state.eval_var]}
end)
Expand Down
Loading

0 comments on commit a5103e7

Please sign in to comment.