Repository navigation
Type printer prints wider types on non_empty_list forms #15837
Description
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)
# => trueAnother 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)
# => falseAnother 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)
# => trueExpected 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
Do not widen list types when printing their last type