Skip to content

Compiler Crash in beam_ssa_pre_codegen with Recursive Elixir Function #9987

@neilberkman

Description

@neilberkman

Describe the bug

When compiling a specific recursive function pattern, the OTP 28 compiler crashes with an internal bad key error in the beam_ssa_pre_codegen pass. The issue appears to be a regression, as the same code compiles successfully on OTP 27.

While the bug is triggered by Elixir source code, the crash occurs deep within the Erlang compiler's SSA backend (beam_ssa_pre_codegen.erl), which is why this issue is being reported to the Erlang/OTP team.

To Reproduce

A complete repository with the reproduction code and further analysis is available here:
https://github.com/neilberkman/otp28_bug_repro

  1. Set up the environment:

    • Erlang/OTP: 28.0.1 (or latest main branch)
    • Elixir: 1.18.x (built on OTP 28)
  2. Clone the reproduction repository:

    git clone https://github.com/neilberkman/otp28_bug_repro.git
    cd otp28_bug_repro
  3. Run the Elixir compiler on the minimal reproduction file:

    elixirc accurate_minimal_repro.ex

Expected behavior

The code should compile successfully, producing a .beam file without any errors.

Affected versions

  • Erlang/OTP 28.0.1

Note: The bug is not present in Erlang/OTP 27.

Additional context

The crash is consistently reproduced when a function has the following combination of features:

  • A recursive tail call.
  • Pattern-matched function clauses for the base and recursive cases.
  • Nested conditional logic (a case statement containing an if statement).
  • An intermediate variable created within the if block that is immediately passed to another function.

Minimal Reproduction Code (accurate_minimal_repro.ex):

defmodule AccurateMinimalRepro do
  defmodule Cursor do
    defstruct [:current, :position]
    
    def from_list(list, opts \\ []) do
      position = Keyword.get(opts, :position, 0)
      %__MODULE__{current: Enum.at(list, position), position: position}
    end
    
    def to_list(%__MODULE__{}), do: []
    
    def move_forward(%__MODULE__{} = cursor), do: cursor
    
    def delete_before(%__MODULE__{} = cursor, _count), do: cursor
    
    def insert_before(%__MODULE__{} = cursor, _items), do: cursor
  end

  def test_function do
    diffs = Cursor.from_list([{:equal, "test"}])
    line_mode_loop(diffs, {0, 0, "", ""}, :never)
  end

  defp line_mode_loop(
          %Cursor{current: nil} = diffs,
          _acc,
          _deadline
        ),
        do: Cursor.to_list(diffs)

  defp line_mode_loop(
          %Cursor{current: this_diff} = diffs,
          {count_delete, count_insert, text_delete, text_insert},
          deadline
        ) do
    {op, text} = this_diff

    result =
      case op do
        :insert ->
          {diffs, count_delete, count_insert + 1, text_delete, text_insert <> text}

        :delete ->
          {diffs, count_delete + 1, count_insert, text_delete <> text, text_insert}

        :equal ->
          diffs2 =
            if count_delete > 0 && count_insert > 0 do
              temp_diffs = Cursor.delete_before(diffs, count_delete + count_insert)
              sub_diff = main_impl(text_delete, text_insert, false, deadline)
              Cursor.insert_before(temp_diffs, sub_diff)
            else
              diffs
            end
          {diffs2, 0, 0, "", ""}

        _ ->
          raise RuntimeError, "Invalid operation"
      end

    {cursor, count_delete, count_insert, text_delete, text_insert} = result

    line_mode_loop(
      Cursor.move_forward(cursor),
      {count_delete, count_insert, text_delete, text_insert},
      deadline
    )
  end

  defp main_impl(_text1, _text2, _check_lines, _deadline) do
    [{:equal, "stub"}]
  end
end

Compiler Crash Output:

== Compilation error in file accurate_minimal_repro.ex ==
** (CompileError) accurate_minimal_repro.ex: internal error in pass beam_ssa_pre_codegen:
exception error: bad key: {b_var,144}
  in function  map_get/2
      called as map_get({b_var,144},
   #{{b_var,63} => {y,147},
     {b_var,106} => {y,153},
     {b_var,108} => {y,152},
     {b_var,112} => {y,151},
     {b_var,114} => {y,150},
     {b_var,120} => {y,149},
     {b_var,122} => {y,148},
     {b_var,154} => {y,158},
     {b_var,155} => {y,159},
     {b_var,156} => {y,160},
     {b_var,157} => {y,161}})
      *** argument 1: not present in map
  in call from beam_ssa_pre_codegen:init_interval/2 (beam_ssa_pre_codegen.erl:3185)
  in call from beam_ssa_pre_codegen:init_intervals/2 (beam_ssa_pre_codegen.erl:3153)
  in call from beam_ssa_pre_codegen:linear_scan/1 (beam_ssa_pre_codegen.erl:2697)
  in call from beam_ssa_pre_codegen:ssa_pre_codegen/3 (beam_ssa_pre_codegen.erl:173)
  in call from beam_ssa_codegen:module/3 (beam_ssa_codegen.erl:259)
  in call from compile:fold_comp/4 (compile.erl:374)
  in call from compile:'-internal_comp/5-anonymous-1-'/5 (compile.erl:386)
  in call from compile:do_comp/4 (compile.erl:261)
    (elixir 1.18.0-dev) lib/kernel/parallel_compiler.ex:483: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/8

Metadata

Metadata

Assignees

Labels

bugIssue is reported as a bugteam:VMAssigned to OTP team VM

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions