Skip to content

CompileError in 1.18+ for a defprotocol with a defstruct that derives another protocol #14158

@benstepp

Description

@benstepp

Elixir and Erlang/OTP versions

Erlang/OTP 27 [erts-15.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Elixir 1.18.1 (compiled with Erlang/OTP 25)

Operating system

macOS 15.2

Current behavior

When compiling a defprotocol with a defstruct that also derives another protocol, there is a compiler error in 1.18+ (1.18.0 and 1.18.1 checked) that did not occur on previous versions.

Example

# hello.exs
defprotocol Hello do
  def name(object)

  # import Kernel
  @derive {Inspect, only: [:name]}
  defstruct [:name]
end

Running elixir hello.exs will cause the following CompileError.

   error: undefined function def/2 (there is no such import)
    │
  6 │   defstruct [
    │   ^^^^^^^^^^^
    │
    └─ hello.exs:6: Inspect.Hello (module)

** (CompileError) hello.exs: cannot compile module Inspect.Hello (errors have been logged)
    hello.exs:6: (file)
    (elixir 1.18.1) hello.exs:6: Inspect.__deriving__/2

A workaround for the CompileError can be to uncomment the import Kernel right before deriving.

This can also be seen with any other derived protocol

defprotocol Hello do
  def name(object)

  @derive JSON.Encoder
  defstruct [:name]
end

defimpl inside vs outside defprotocol body

After some more investigation on protocol implementation unrelated to deriving the following does not work in 1.17 or 1.18, and maybe that was always intended to be the case. (unless you import Kernel for the correct def somewhere)

defprotocol Hello do
  def name(object)

  defstruct [:name]

  # import Kernel
  defimpl Inspect do
    # import Kernel
    def inspect(_struct, _opts) do
      "hello"
    end
  end
end
    error: undefined function def/2 (there is no such import)
    │
  8 │     def inspect(_struct, _opts) do
    │     ^
    │
    └─ hello.exs:9:5: Inspect.Hello (module)

** (CompileError) hello.exs: cannot compile module Inspect.Hello (errors have been logged)

Moving the defimpl outside of the defprotocol body causes it to work in both 1.17 and 1.18, so maybe defimpl in the body was supposed to work?

defprotocol Hello do
  def name(object)

  defstruct [:name]
end

defimpl Inspect, for: Hello do
  def inspect(_struct, _opts) do
    "hello"
  end
end

Expected behavior

This was discovered when upgrading from 1.17 to 1.18, and it was found that deriving Inspect inside of defprotocol caused a CompileError. The previous behaviour where it compiled without errors would be expected.

After investigation though, I think a little more with protocol implementation is inconsistent. Right now I think it looks something like:

impl method works in 1.17 works in 1.18
deriving inside defprotocol body yes no
defimpl inside defprotocol body no no
defimpl outside of defprotocol body yes yes
deriving inside defmodule body yes yes
defimpl inside defmodule body yes yes
defimpl outside of defmodule body yes yes

I think defimpl needs to import to the correct Kernel.def to make all of the ways to implement a protocol consistent when used on a defprotocol instead of a defmodule, but I'm not sure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions