Skip to content

Commit

Permalink
Defd_: Introduce @moduledx_ attribute with defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
arnodirlam committed Dec 9, 2024
1 parent e1a60d3 commit 7619dcd
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 103 deletions.
5 changes: 1 addition & 4 deletions lib/dx/date_time.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ defmodule Dx.DateTime do

use Dx.Defd_

@impl true
def __dx_fun_info(_fun_name, _arity) do
%FunInfo{args: %{all: :preload_scope}}
end
@moduledx_ args: %{all: :preload_scope}

defscope after?(left, right, generate_fallback) do
quote do: {:gt, unquote(left), unquote(right), unquote(generate_fallback.())}
Expand Down
5 changes: 1 addition & 4 deletions lib/dx/defd/kernel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ defmodule Dx.Defd.Kernel do

use Dx.Defd_

@impl true
def __dx_fun_info(_fun_name, _arity) do
%FunInfo{args: %{all: :preload_scope}}
end
@moduledx_ args: %{all: :preload_scope}

defscope unquote(:==)({:error, _left}, _right, generate_fallback) do
{:error, generate_fallback.()}
Expand Down
5 changes: 1 addition & 4 deletions lib/dx/defd/string/chars.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,5 @@ defmodule Dx.Defd.String.Chars do

use Dx.Defd_

@impl true
def __dx_fun_info(_fun_name, _arity) do
%FunInfo{args: %{all: :preload_scope}}
end
@moduledx_ args: %{all: :preload_scope}
end
200 changes: 120 additions & 80 deletions lib/dx/defd_.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,63 @@ defmodule Dx.Defd_ do
@moduledoc """
Used to make existing libraries compatible with `Dx.Defd`.
## Usage
## Defining functions
There are two ways to provide function information:
Define functions using `defd_/2`. The `_` stands for *basic* or *native*.
`defd_` functions are not recompiled by the Dx compiler.
They have to return either `{:ok, result}` or `{:not_loaded, data_reqs}`.
See `Dx.Defd.Result` for more information and Enum-like functions to work with results.
1. Implementing the `__dx_fun_info/2 callback:
The input arguments can be a `Dx.Scope` struct or a `Dx.Defd.Fn` struct.
If you don't want to handle these internal structs, you can tell the compiler
to load/unwrap them by providing function information (see next section).
## Function information
```elixir
defmodule MyExt do
use Dx.Defd_
@impl true
def __dx_fun_info(fun_name, arity) do
%FunInfo{args: [:preload_scope, %{}, :final_args_fn]}
@dx_ args: [:preload_scope, :fn], warn_not_ok: "Be careful!"
defd_ map(enum, mapper) do
# ...
end
end
```
2. Using `@dx_` module attributes before function definitions:
### Options
- `args` - list or map of argument indexes mapping to argument information
- List format: `[:preload_scope, %{}, :fn]` - each element maps to an argument position
- Map format with special keys:
- Integer keys (0-based): `%{0 => :preload_scope}` - specific argument positions
- `:first` - applies to first argument
- `:last` - applies to last argument
- `:all` - applies to all arguments unless overridden
Precedence (highest to lowest):
1. Specific integer positions
2. `:first`/`:last` positions
3. `:all` default
Argument information options:
- `:atom_to_scope` - whether to wrap atoms in `Dx.Scope.all/1`
- `:preload_scope` - tells the compiler to load any scopes passed via this argument
- `:fn` - tells the compiler to unwrap any Dx-specific function definitions
- `{:fn, arity: 2, warn_not_ok: "Can't load data here"}` - pass more information about the function
- `:final_args_fn` - like `fn` but assumes that no scopes can be passed to the function in this argument
- `{:final_args_fn, arity: 2, warn_always: "Don't use this function"}` - pass more information about the function
- `%{}` or `[]` - placeholder for an argument without any special information
Additional options:
- `warn_not_ok` - compiler warning to display when the function possibly loads data
- `warn_always` - compiler warning to display when the function is used
## Compiler annotations & callbacks
There are three ways to provide function information for the Dx compiler:
1. Using `@dx_` module attributes before function definitions:
```elixir
defmodule MyExt do
Expand All @@ -37,85 +76,68 @@ defmodule Dx.Defd_ do
end
```
Both can be combined. Annotations have precedence over `__dx_fun_info/2` clauses.
2. Using the `@moduledx_` module attribute for module-wide defaults (can only be set once per module):
```elixir
defmodule MyExt do
use Dx.Defd_
# Specific function pattern in __dx_fun_info
def __dx_fun_info(:special_case, 2) do
%FunInfo{args: [:preload_scope, :final_args_fn]}
end
@moduledx_ args: %{all: :atom_to_scope},
warn_always: "This module is deprecated"
end
```
# Fallback for all functions
def __dx_fun_info(_fun, _arity) do
%FunInfo{args: %{all: :atom_to_scope}}
end
3. Implementing the `__dx_fun_info/2` callback:
# Specific function overrides with @dx_
@dx_ args: [:preload_scope, :fn]
defd_ process_data(scope, callback) do
# This function's settings override the fallback
```elixir
defmodule MyExt do
use Dx.Defd_
@impl true
def __dx_fun_info(fun_name, arity) do
%FunInfo{args: [:preload_scope, %{}, :final_args_fn]}
end
end
```
## Options
All three approaches can be combined. The precedence order (highest to lowest) is:
Return a map with the following keys:
1. `@dx_` function-specific annotations
- merged into `@moduledx_` defaults for that function
2. `__dx_fun_info/2` callback implementations
- always overrides `@moduledx_` defaults
3. `@moduledx_` module-wide defaults
- `args` - list or map of argument indexes mapping to argument information
- List format: `[:preload_scope, %{}, :fn]` - each element maps to an argument position
- Map format with special keys:
- Integer keys (0-based): `%{0 => :preload_scope}` - specific argument positions
- `:first` - applies to first argument
- `:last` - applies to last argument
- `:all` - applies to all arguments unless overridden
```elixir
defmodule MyExt do
use Dx.Defd_
Precedence (highest to lowest):
1. Specific integer positions
2. `:first`/`:last` positions
3. `:all` default
# Module-wide defaults (lowest precedence)
# Must be set once with all defaults
@moduledx_ args: %{all: :preload_scope},
warn_always: "Module under development"
Argument information options:
- `:atom_to_scope` - whether to wrap atoms in `Dx.Scope.all/1`
- `:preload_scope` - tells the compiler to load any scopes passed via this argument
- `:fn` - tells the compiler to unwrap any Dx-specific function definitions
- `{:fn, arity: 2, warn_not_ok: "Can't load data here"}` - pass more information about the function
- `:final_args_fn` - like `fn` but assumes that no scopes can be passed to the function in this argument
- `{:final_args_fn, arity: 2, warn_always: "Don't use this function"}` - pass more information about the function
- `%{}` or `[]` - placeholder for an argument without any special information
# Function pattern in __dx_fun_info (middle precedence)
def __dx_fun_info(:special_case, 2) do
%FunInfo{args: [:preload_scope, :final_args_fn]}
end
Additional options:
- `warn_not_ok` - compiler warning to display when the function possibly loads data
- `warn_always` - compiler warning to display when the function is used
# Function-specific override (highest precedence)
@dx_ args: [:preload_scope, :fn]
defd_ process_data(scope, callback) do
# This function's settings override both __dx_fun_info and @moduledx_
end
## Examples
# Uses __dx_fun_info(:special_case, 2) settings
defd_ special_case(a, b) do
# ...
end
```elixir
# Using list format
%FunInfo{args: [:preload_scope, %{}, :final_args_fn]}
# Using map format with specific positions
%FunInfo{args: %{0 => :preload_scope, 2 => :final_args_fn}}
# Using special keys
%FunInfo{args: %{
first: :preload_scope,
last: :final_args_fn,
all: :atom_to_scope
}}
# Complex function information
%FunInfo{
args: [
:preload_scope,
%{},
{:fn, arity: 2, warn_not_ok: "Can't load data here"}
],
warn_always: "Use with caution"
}
# Falls back to default @moduledx_ settings
defd_ other_function(x) do
# ...
end
end
```
"""

Expand All @@ -127,6 +149,8 @@ defmodule Dx.Defd_ do
alias Dx.Defd_.FunInfo

import Dx.Defd_

unquote(__MODULE__).__init__(__MODULE__)
end
end

Expand Down Expand Up @@ -279,22 +303,29 @@ defmodule Dx.Defd_ do
@defd__exports_key :__defd__exports__

@doc false
def __define__(_env, _kind, _name, _arity, _defaults, nil) do
def __init__(module) do
Module.put_attribute(module, @defd__exports_key, %{})
Module.put_attribute(module, :before_compile, __MODULE__)
end

@doc false
def __define__(%Macro.Env{module: module}, _kind, _name, _arity, _defaults, nil) do
if is_nil(Module.get_attribute(module, @defd__exports_key)) do
__init__(module)
end

:ok
end

def __define__(%Macro.Env{module: module} = env, kind, name, arity, defaults, opts) do
exports =
if exports = Module.get_attribute(module, @defd__exports_key) do
exports
else
Module.put_attribute(module, :before_compile, __MODULE__)
%{}
end

fun_info =
try do
Dx.Defd_.FunInfo.new!(opts || [], %{module: module, fun_name: name, arity: arity})
Dx.Defd_.FunInfo.new!(
Module.get_attribute(module, :moduledx_, []),
%{arity: arity},
opts || [],
%{module: module, fun_name: name}
)
rescue
e ->
compile_error!(
Expand All @@ -315,6 +346,14 @@ defmodule Dx.Defd_ do
fun_info: fun_info
}

exports =
if exports = Module.get_attribute(module, @defd__exports_key) do
exports
else
Module.put_attribute(module, :before_compile, __MODULE__)
%{}
end

exports = Map.put_new(exports, {name, arity}, current_export)

Module.put_attribute(module, @defd__exports_key, exports)
Expand All @@ -327,8 +366,9 @@ defmodule Dx.Defd_ do

@doc false
defmacro __before_compile__(env) do
defd__exports = Module.get_attribute(env.module, @defd__exports_key)
defd__exports = Module.get_attribute(env.module, @defd__exports_key, %{})
moduledx_ = Module.get_attribute(env.module, :moduledx_, [])

Dx.Defd_.Compiler.__compile__(env, defd__exports)
Dx.Defd_.Compiler.__compile__(env, moduledx_, defd__exports)
end
end
11 changes: 9 additions & 2 deletions lib/dx/defd_/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Dx.Defd_.Compiler do

alias Dx.Defd.Ast

def __compile__(%Macro.Env{module: module}, fun_infos) do
def __compile__(%Macro.Env{module: module}, moduledx_, fun_infos) do
existing_clauses =
case Module.get_definition(module, {:__dx_fun_info, 2}) do
{:v1, :def, _meta, clauses} ->
Expand Down Expand Up @@ -43,7 +43,14 @@ defmodule Dx.Defd_.Compiler do
[]
end)

(annotated_clauses ++ existing_clauses)
fallback_clause =
quote do
def __dx_fun_info(_fun_name, _arity) do
unquote(Macro.escape(moduledx_))
end
end

(annotated_clauses ++ existing_clauses ++ [fallback_clause])
|> Ast.block()
end
end
15 changes: 9 additions & 6 deletions lib/dx/defd_/fun_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,22 @@ defmodule Dx.Defd_.FunInfo do
%FunInfo{args: [%ArgInfo{atom_to_scope: true}], warn_always: "WARNING", arity: 1}
"""

@spec new!(input(), keyword() | %{atom() => term()}) :: t()
def new!(fun_info \\ %FunInfo{}, extra_fields \\ [])
@spec new!(input(), keyword() | %{atom() => term()}, keyword() | %{atom() => term()},
keyword() | %{atom() => term()}) :: t()
def new!(fields \\ [], extra_fields1 \\ [], extra_fields2 \\ [], extra_fields3 \\ [])

def new!(%FunInfo{} = fun_info, extra_fields) do
def new!(%FunInfo{} = fun_info, extra_fields1, extra_fields2, extra_fields3) do
fun_info
|> struct!(extra_fields)
|> struct!(extra_fields1)
|> struct!(extra_fields2)
|> struct!(extra_fields3)
|> args!()
end

def new!(fields, extra_fields) do
def new!(fields, extra_fields1, extra_fields2, extra_fields3) do
FunInfo
|> struct!(fields)
|> new!(extra_fields)
|> new!(extra_fields1, extra_fields2, extra_fields3)
end

defp args!(%FunInfo{arity: nil} = fun_info), do: fun_info
Expand Down
4 changes: 1 addition & 3 deletions lib/dx/enum.ex
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,7 @@ defmodule Dx.Enum do
}
end

def __dx_fun_info(_fun_name, _arity) do
%FunInfo{args: %{first: [:atom_to_scope, :preload_scope], last: :final_args_fn}}
end
@moduledx_ args: %{first: [:atom_to_scope, :preload_scope], last: :final_args_fn}

defscope all?(enumerable, fun, _generate_fallback) do
quote do: {:not, {:any?, {:filter, unquote(enumerable), {:not, unquote(fun)}}}}
Expand Down
Loading

0 comments on commit 7619dcd

Please sign in to comment.