Existing issue
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
Existing issue
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.Descrprinter emitsnon_empty_list/2representation that when used directly produces a wider type.The list representation stores the type of the terminator. The
non_empty_list/2constructor 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
Another example:
reconstructed type admits
[:not_an_integer]while original admits proper or improper lists of integersAnother example with gradual types:
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