Elixir and Erlang/OTP versions
Erlang/OTP 28 [erts-16.2] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]
Elixir 1.20.0-rc.4 (116f8f4) (compiled with Erlang/OTP 28)
Operating system
any
Current behavior
Kernel.Utils.defguard always expands the guard expression with context: :guard even when the __CALLER__ context is not :guard
|
def defguard(args, expr, env) do |
|
{^args, vars} = extract_refs_from_args(args) |
|
env = :elixir_env.with_vars(%{env | context: :guard}, vars) |
|
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env) |
|
|
|
quote do |
|
case Macro.Env.in_guard?(__CALLER__) do |
|
true -> |
|
unquote(literal_quote(unquote_every_ref(expr, vars), [])) |
|
|
|
false -> |
|
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true)) |
|
end |
|
end |
|
end |
This means that context aware macros like Kernel.and and Kernel.or do not expand to build_boolean_check when guard is used as a function. They always expand to :erlang.andalso and :erlang.orelse and miss optimize_boolean path.
The bug is affecting runtime behavior
defmodule G do
defguard g(x) when x and true
end
import G
g(nil)
** (ArgumentError) argument error: nil
iex:3: (file)
ArgumentError comes from :badarg throw by :erlang.andalso
:erlang.andalso(nil, true)
** (ArgumentError) argument error: nil
iex:3: (file)
Compare that to usual elixir BadBooleanError
nil and true
** (BadBooleanError) expected a boolean on left-side of "and", got:
nil
iex:3: (file)
Expected behavior
I think the guard expression should be expanded twice, once in :guard, once in nil context and the approperiate version used depending on Macro.Env.in_guard?(__CALLER__). It should be safe, state and env is discarded on expansion anyway. Compiler tracers firing twice may be an issue though.
Alternative is documenting the current behavior
Elixir and Erlang/OTP versions
Erlang/OTP 28 [erts-16.2] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]
Elixir 1.20.0-rc.4 (116f8f4) (compiled with Erlang/OTP 28)
Operating system
any
Current behavior
Kernel.Utils.defguardalways expands the guard expression withcontext: :guardeven when the__CALLER__context is not:guardelixir/lib/elixir/lib/kernel/utils.ex
Lines 331 to 345 in 5e44c78
This means that context aware macros like
Kernel.andandKernel.ordo not expand tobuild_boolean_checkwhen guard is used as a function. They always expand to:erlang.andalsoand:erlang.orelseand missoptimize_booleanpath.The bug is affecting runtime behavior
ArgumentErrorcomes from:badargthrow by:erlang.andalsoCompare that to usual elixir BadBooleanError
Expected behavior
I think the guard expression should be expanded twice, once in
:guard, once innilcontext and the approperiate version used depending onMacro.Env.in_guard?(__CALLER__). It should be safe, state and env is discarded on expansion anyway. Compiler tracers firing twice may be an issue though.Alternative is documenting the current behavior