emeryotopalik

emeryotopalik

Strange exception handling by guard clause

Hello. I am experiencing an unexpected behavior with the way Elixir is handling a guard clause exception. Take the following:

defmodule Foo do
     def eval(x, percentage) when rem(x, 100) < percentage, do: true
     def eval(_, _), do: false
end

Foo.eval(nil, 20) # Sometimes this is false, sometimes it is true

I have a function very similar to the one above running in my application. Occasionally, I pass x = nil to the function. I would expect there to ALWAYS be an Arithmetic Exception stemming from calling rem(nil, 100) and thus the guard would fail and we would catch the next eval function head.

But, that is not the case. I’ve tried digging into the Elixir source code to decipher how exceptions are handled in guards, but I get a bit turned around. My best guess is that the exception is being compared in the < evaluation itself, and thus the flakiness? I truly do not know.

I do understand that the way to fix this would be to first check is_integer(x). I am merely trying to understand why the exception is being handled as I expect in the first place.

Marked As Solved

krstfk

krstfk

I believe this has to do with this bug :https://github.com/erlang/otp/issues/5401 which has been fixed with https://github.com/erlang/otp/pull/5409 and was part of the erts 12.1.4 (I believe released with Erlang 24.1.5).
I could not reproducer using your repo :

$ iex --version
Erlang/OTP 24 [erts-12.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]

IEx 1.12.3 (compiled with Erlang/OTP 24)

$mix test
.....

Finished in 4.2 seconds (0.00s async, 4.2s sync)
1 doctest, 4 tests, 0 failures

Also Liked

eksperimental

eksperimental

I can confirm this issue. As I have managed to reproduce it @emeryotopalik

And it sounds like a serious one. And when it fails, it fails repeatedly for a while until it may go back to normal, and then it may fail again.

I am running Arch Linux 64bits, and my elixir --version is:
OTP 24.1.2

$ elixir --version
Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Elixir 1.12.2 (compiled with Erlang/OTP 24)

mix test fails big time:

$ mix test
Compiling 3 files (.ex)
Compiling lib/bar.ex (it's taking more than 10s)
Generated debug_me app

  1) test Compile time (FooTest)
     test/foo_test.exs:19
     Assertion with == failed
     code:  assert report(Bar.result()) == [false: times]
     left:  [{false, 1970374}, {true, 8029626}]
     right: [false: 10000000]
     stacktrace:
       test/foo_test.exs:22: (test)

.

  2) test Runtime (FooTest)
     test/foo_test.exs:12
     Assertion with == failed
     code:  assert report(result) == [false: times]
     left:  [
              {false, 606},
              {true, 640},
              {false, 588},
              {true, 1264},
              {false, 597},
              {true, 528},
              {false, 194331},
              {true, 2000},
              {false, 1333},
              {true, 1333},
              {false, 666},
              {true, 9796114}
            ]
     right: [false: 10000000]
     stacktrace:
       test/foo_test.exs:16: (test)
..

Finished in 5.1 seconds (0.00s async, 5.1s sync)
1 doctest, 4 tests, 2 failures

Randomized with seed 934406
$ MIX_ENV=test iex -S mix
Erlang/OTP 24 [erts-12.1.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> for chunk <- Enum.chunk_by(Bar.result(), & &1), do: {hd(chunk), length(chunk)}
[false: 1970374, true: 8029626]

iex(2)> Enum.map(1..10_000_000, fn _i -> Foo.eval(nil, 20) end) |> Foo.report()       
[false: 10000000]

iex(3)> Enum.map(1..10_000_000, fn _i -> Foo.eval(nil, 20) end) |> Foo.report()
[false: 10000000]

iex(4)> Enum.map(1..10_000_000, fn _i -> Foo.eval(nil, 20) end) |> Foo.report()
[false: 10000000]

Now I run it with OTP 23.3.4.9 , mix testpass all tests,
here are the commands on IEx:

$ iex -S mix
Erlang/OTP 23 [erts-11.2.2.8] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> for chunk <- Enum.chunk_by(Bar.result(), & &1), do: {hd(chunk), length(chunk)}
[false: 10000000]

iex(2)> Enum.map(1..10_000_000, fn _i -> Foo.eval(nil, 20) end) |> Foo.report()
[false: 10000000]

…and it’s all good.

So my quick assumption is that this is due the the JIT compiler introduced in OTP24

I have created a project for things like this, where I test an Elixir project in every Elixir/OTP version combination.
and suprisingly it does not fail for OTP 24 with JIT. (it only fails with OTP 17, and it does not even compile, so that is unrelated).

https://github.com/eksperimental/debug_me/runs/4491828158?check_suite_focus=true

the source code of Elixir project can be found here:
GitHub - eksperimental/debug_me at emeryotopalik


UPDATE 1:

And Noticed that when it fails it fails after ~600 results (or multiple of this number), after running MIX_ENV=test mix do compile --force, test multiple times:

these 2 are from the same compilation:

# Compile times:  1998
[{false, 1246}, {true, 1186}, {false, 666}, {true, 1126}, {false, 666}, {true, 5110}]

# Run time: Note that 1998 is 666 * 3
[{false, 1998}, {true, 8002}]
----------------

then I got:

# Runtime (but Compile time did not fail)
[{false, 606}, {true, 999394}]

--------
# Compiletime
 [{false, 665}, {true, 99335}]

---------
# Compile time
[{false, 665}, {true, 99335}]
al2o3cr

al2o3cr

Exceptions aren’t propagated outside the guard, they are considered “failure to match”:

If an arithmetic expression, a Boolean expression, a short-circuit expression, or a call to a guard BIF fails (because of invalid arguments), the entire guard fails. If the guard was part of a guard sequence, the next guard in the sequence (that is, the guard following the next semicolon) is evaluated.

(from the Erlang reference manual)

I’d guess the intent was to avoid having to spam is_integer and similar checks.

trisolaran

trisolaran

I tried your code. Foo.eval(nil, 20) always returns false to me.

iex(36)> 1..10000 |> Enum.map(fn _ -> Foo.eval(nil, 20) end) |> Enum.uniq
[false]
stefanluptak

stefanluptak

Same here:

iex(12)> Enum.all?(1..100000000, fn _i -> Foo.eval(nil, 20) == false end)
true
emeryotopalik

emeryotopalik

I wonder if this is maybe only reproducible in an application. When I run this code in iex, I agree it always returns false.

Where Next?

Popular in Questions Top

lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New

We're in Beta

About us Mission Statement