Skip to content

Type printer prints wider types on non_empty_list forms #15837

Description

@lukaszsamson

Existing issue

  • I have searched existing issues and could not find a duplicate.

Elixir and Erlang/OTP versions

Erlang/OTP 28 [erts-16.4.0.1] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Interactive Elixir (1.21.0-dev)

Operating system

any

Current behavior

Module.Types.Descr printer emits non_empty_list/2 representation that when used directly produces a wider type.
The list representation stores the type of the terminator. The non_empty_list/2 constructor instead accepts a tail type and folds any list-valued part of that tail into the element type.

Repro:
When printed and reconstructed the reconstructed type admits proper lists when the original type only admits improper ones

import Module.Types.Descr

type =
  non_empty_list(
    none(),
    opt_negation(list(term()))
  )

to_quoted_string(type)
# => "non_empty_list(term(), not empty_list())"

reconstructed =
  non_empty_list(
    term(),
    opt_negation(empty_list())
  )

equal?(type, reconstructed)
# => false

subtype?(type, reconstructed)
# => true

subtype?(reconstructed, type)
# => false

proper_list = non_empty_list(integer())

disjoint?(type, proper_list)
# => true

subtype?(proper_list, reconstructed)
# => true

Another example:
reconstructed type admits [:not_an_integer] while original admits proper or improper lists of integers

original =
  non_empty_list(
    integer(),
    opt_negation(non_empty_list(term(), term()))
  )

to_quoted_string(original)
# => "non_empty_list(integer(), term())"

reconstructed = non_empty_list(integer(), term())

equal?(original, reconstructed)
# => false

subtype?(original, reconstructed)
# => true

subtype?(reconstructed, original)
# => false

Another example with gradual types:

original =
  opt_difference(
    non_empty_list(none(), term()),
    list(dynamic())
  )

to_quoted_string(original)
# => "dynamic(non_empty_list(term())) or non_empty_list(term(), not empty_list())"

reconstructed =
  opt_union(
    dynamic(non_empty_list(term())),
    non_empty_list(term(), opt_negation(empty_list()))
  )

to_quoted_string(reconstructed)
# => "non_empty_list(term(), term())"

equal?(original, reconstructed)
# => false

subtype?(original, reconstructed)
# => true

subtype?(reconstructed, original)
# => false

proper_list = non_empty_list(integer())

subtype?(proper_list, original)
# => false

subtype?(proper_list, reconstructed)
# => true

Expected behavior

Printed form should reconstruct the same type. Widening in the constructor is intentional and documented. Printer only documents "print the last type as term() if it only misses non empty lists." which suggests the current behavior is an unsafe optimization

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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