Elixir v1.20.0-rc.5 released

This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.

1. Enhancements

EEx

  • [EEx] Optimize compiler by flattening expr list only once

Elixir

  • [Base] Optimize Base validation functions by using SWAR techniques
  • [Float] Optimize Float.round/2 by avoiding big integers
  • [Inspect] Increase inspect limit to help print deeply nested data structures
  • [Inspect] Support printing Erlang records (using Erlang notation)
  • [Kernel] Add occurrence typing on case, cond, and with
  • [Registry] Switch {:duplicate, :key} key_ets to ordered_set with composite keys
  • [String] SWAR-optimize ASCII fast paths in String.length/1 and String.slice/3

ExUnit

  • [ExUnit] Show remaining runs when using --repeat-until-failure

IEx

  • [IEx.Helpers] Add source/1

Mix

  • [mix app.tree] Support --output option
  • [mix deps.tree] Support --output option
  • [mix help] Support printing docs for types and callbacks
  • [mix format] Support --no-compile option
  • [mix source] Add mix source MODULE to print or open a given module/function location

2. Potential breaking changes

Elixir

  • [Kernel] Disallow raw CR line ending in strings, comments and after ? for security reasons

3. Bug fixes

Elixir

  • [Kernel] Fix a compiler crash when importing a module with only: :sigils option when the imported module exports non-sigil symbols with sigil_ prefix
  • [Kernel] Reject negative Duration in to_timeout/1
  • [Macro] Fix generation of heredocs in Macro.to_string/1 with escaped trailing newline
  • [Path] Consistently return path as binary in Path.relative_to_cwd/2
  • [Stream] Raise in Stream.cycle/1 when enumerable reduce call yields no elements
  • [String] Support empty pattern list in String.count/2

Logger

  • [Logger] Persist log level to app env in Logger.configure/1

Mix

  • [Mix] Use non_executable_binary_to_term on loopback pubsub
  • [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
28 Likes

As usual, this release has additional type checks and performance improvements. Please give it a try. We expect only one additional RC after this one with any pending fixes, so we can release v1.20.0.

17 Likes

Congratulations on the release! Are you interested in false positive type warnings - should we report them as bugs? E.g., I have a piece of HEEX where the type checker complains but stop complaining if I simply change the order

                  <.yinsh_score_ring
                    :for={index <- 1..3}
                    # no complaints if `earned?` is placed here instead
                    # earned?={index <= player.yinsh_rings_removed}
                    id={"#{@id}-#{player.color}-ring-score-#{index}"}
                    color={player.color}
                    earned?={index <= player.yinsh_rings_removed}
                  />

The type warning:

     type warning found at:
     │
 270 │                 earned?={index <= player.yinsh_rings_removed}
     │                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     │
     └─ lib/playgipf_web/components/game_components.ex:270: PlaygipfWeb.GameComponents."game_topbar (overridable 1)"/1

     warning: comparison with structs found:

         index <= player.yinsh_rings_removed

     given types:

         dynamic(
           %{
             ...,
             __struct__:
               Date or DateTime or Decimal or NaiveDateTime or Phoenix.LiveComponent.CID or Postgrex.Copy or
                 Postgrex.Query or Postgrex.TextQuery or Time or URI or Version or Version.Requirement
           } or atom() or bitstring() or empty_list() or float() or integer() or
             non_empty_list(term(), term())
         ) <= dynamic()

     where "index" was given the type:

         # type: dynamic(
           %{
             ...,
             __struct__:
               Date or DateTime or Decimal or NaiveDateTime or Phoenix.LiveComponent.CID or Postgrex.Copy or
                 Postgrex.Query or Postgrex.TextQuery or Time or URI or Version or Version.Requirement
           } or atom() or bitstring() or empty_list() or float() or integer() or
             non_empty_list(term(), term())
         )
         # from: lib/playgipf_web/components/position_components/position_panel.ex:441
         to_string(index)

     where "player" was given the types:

         # type: dynamic(%{..., game_type: term()})
         # from: lib/playgipf_web/components/position_components/position_panel.ex:431
         player.game_type == :yinsh

         # type: dynamic(%{..., game_type: :yinsh})
         # from: lib/playgipf_web/components/position_components/position_panel.ex:431
         player.game_type == :yinsh

         # type: dynamic(%{..., game_type: :yinsh, yinsh_setup?: false})
         # from: lib/playgipf_web/components/position_components/position_panel.ex:431
         not player.yinsh_setup?

         # type: dynamic(%{
           ...,
           color:
             %{
               ...,
               __struct__:
                 Date or DateTime or Decimal or NaiveDateTime or Phoenix.LiveComponent.CID or Postgrex.Copy or
                   Postgrex.Query or Postgrex.TextQuery or Time or URI or Version or Version.Requirement
             } or atom() or bitstring() or empty_list() or float() or integer() or
               non_empty_list(term(), term()),
           game_type: :yinsh,
           yinsh_setup?: false
         })
         # from: lib/playgipf_web/components/position_components/position_panel.ex:441
         to_string(player.color)

     Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison.
4 Likes

That’s unexpected, please file a report!

3 Likes

Done: False positive type warning depending on a HEEX clause ordering · Issue #15370 · elixir-lang/elixir · GitHub

2 Likes

I think this warning was introduced with this version:

warning: use `or` instead of `||` for boolean checks
is_nil(id) || Enum.member?(deleted_ids, id)

I think this is not good. || wil short-circuit but or will evaluate the right-hand side.

If right-hand side is computationally intense, this is encouraging less performant code.

Weird, I think I always thought both short-circuited :flushed_face:, but it seem neither do?

iex(1)> false or IO.puts("hi")
hi
:ok
iex(2)> false || IO.puts("hi")
hi
:ok

Requires only the left operand to be a boolean since it short-circuits.

Need to use true for short-circuit:

iex(1)> true or IO.puts("hi")
true
iex(2)> true || IO.puts("hi")
true
1 Like

LOL… oh boy, uhhhh… who are you responding to? I never said anything!

:upside_down_face:

They both seem to short circuit, though:

iex(3)> true or 0..100_000_000 |> Enum.map(& &1)
true
iex(4)> true || 0..100_000_000 |> Enum.map(& &1)
true

Both of those return instantly, in IEx, at least.

Am I just completely misunderstand what short circuiting is?

1 Like

They do both indeed short-circuit, and the docs for both say they do. The difference is or requires the left side to be a boolean and it can be used in guards, while || does a “truthy” check of the left hand side and cannot be used in guards.

THAT part I knew. I still haven’t tried out this rc yet but does the type system now warn if both sides aren’t bools? That always sorta bugged me.

Thanks @jswanner
I don’t know why I have thought (for years) that || short-circuits and or does not short-circuit.
Apologies for the noise.

Going by what @slouchpie shared, I believe it warns if the left side is a boolean when || is used as or can be used instead. I tried to recreate that warning but failed to do so

1 Like

Oh I got that, and I’m quite happy about that (|| vs or vs && vs and is one of my nits).

My apologies, I read your “aren’t” as “are”: “both sides are bools”

That said, the Elixir docs for or explicitly show a use where the right hand side is not a boolean. In guards, or uses Erlang’s orelse, in the Erlang docs they have the following:

Before Erlang/OTP R13A, Expr2 was required to evaluate to a Boolean value, and as a consequence, andalso and orelse were not tail-recursive.

1.20.0-rc.5 found a number of unnecessary conditionals in heex templates, very cool!

2 Likes

[Question] try with many independent problems only warns once. On current main (b58e84352), the behaviour of try’s diagnostics changed; once one try alternative sets context.failed, later independent alternatives stop reporting diagnostics:

1. Two rescue bodies with independent bad calls:
defmodule A1R1 do
  def f do
    try do
      raise "oops"
    rescue
      _ in RuntimeError -> String.length(1)
      _ in ArgumentError -> String.length(2)
    end
  end
end

v1.19.5 (d33fd8e41):

(none)

current main / v1.20 RC (b58e84352):

warning: incompatible types given to String.length/1:
    String.length(1)
given types:
    -integer()-
but expected one of:
    binary()
└─ a1_r1.ex:6:35: A1R1.f/0
2. Failed rescue body followed by undefined exception head:
defmodule A1R2 do
  def f do
    try do
      raise "oops"
    rescue
      _ in RuntimeError -> String.length(1)
      e in UnknownError -> e
    end
  end
end

v1.19.5 (d33fd8e41):

warning: struct UnknownError is undefined (module UnknownError is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
└─ a1_r2.ex:7:9: A1R2.f/0

current main / v1.20 RC (b58e84352):

warning: incompatible types given to String.length/1:
    String.length(1)
given types:
    -integer()-
but expected one of:
    binary()
└─ a1_r2.ex:6:35: A1R2.f/0
3. Failed rescue body before catch body:
defmodule A1R3 do
  def f do
    try do
      throw(:x)
    rescue
      _ in RuntimeError -> String.length(1)
    catch
      :throw, :x -> Integer.to_string(:atom)
    end
  end
end

v1.19.5 (d33fd8e41):

warning: the call to integer_to_binary/1 will fail with a 'badarg' exception
└─ a1_r3.ex:8:29

warning: incompatible types given to Integer.to_string/1:
    Integer.to_string(:atom)
given types:
    -:atom-
but expected one of:
    integer()
└─ a1_r3.ex:8:29: A1R3.f/0

current main / v1.20 RC (b58e84352):

warning: incompatible types given to String.length/1:
    String.length(1)
given types:
    -integer()-
but expected one of:
    binary()
└─ a1_r3.ex:6:35: A1R3.f/0
4. else body warning before rescue body:
defmodule A1R4 do
  def f(x) do
    try do
      x
    rescue
      _ in RuntimeError -> Integer.to_string(:atom)
    else
      :ok -> String.length(1)
    end
  end
end

v1.19.5 (d33fd8e41):

warning: incompatible types given to Integer.to_string/1:
    Integer.to_string(:atom)
given types:
    -:atom-
but expected one of:
    integer()
└─ a1_r4.ex:6:36: A1R4.f/1

current main / v1.20 RC (b58e84352):

warning: incompatible types given to String.length/1:
    String.length(1)
given types:
    -integer()-
but expected one of:
    binary()
└─ a1_r4.ex:8:21: A1R4.f/1

Concern

So, try seems to suppress diagnostics after the 1st failing alternative. Equivalent non-try structures still behave as expected (case clauses, multiple catch clauses, or multiple else clauses); they report independent clause warnings.

Investigation

The behaviour appears to come from lib/elixir/lib/module/types/expr.ex:

  • try handles else first, then reduces rescue/catch blocks sequentially;
  • Rescue clauses are checked by an inline nested Enum.reduce;
  • of_rescue/9 resets variables after the rescue body with Of.reset_vars/2, but it does not isolate or restore failed;
  • Normal clause checking goes through of_clauses_fun/7, which wraps each clause with reset_failed/2 and set_failed/2, allowing independent diagnostics while preserving the final failed state.

@evadne please open up an issue!

1 Like

Others have already explained that both short-circuit but I want to add that said warning is not coming from Elixir. :slight_smile: