[m-rev.] for review: expand the class of allowed coercions

Peter Wang novalazy at gmail.com
Fri Jul 31 16:18:42 AEST 2026


On Thu, 30 Jul 2026 22:35:27 +0200 "Zoltan Somogyi" <zoltan.somogyi at runbox.com> wrote:
> For review by Peter.
> 
> I am in two minds about whether the small database built up
> by this diff, mapping type_ctors to the set of their invariant type
> parameters, should be added to the typecheck_info structure, or not.
> 
> On the one hand, adding it to the typecheck_info can prevent
> redundant computations, which can be non-trivial if we ever get
> coercions between types whose definitions are large. (This can
> happen after this diff.)
> 
> On the other hand, expanding typecheck_(sub_)_info imposes a
> cost on all compiler invocations, even the ones that do not involve
> any coercions.
> 
> I am leaning towards leaving things as they are, with a note to
> revisit the issue if and when coerces of large types become
> more commonplace.

Yes, I agree.

> Keep a map from type_ctors to their invariant tparams.
> 
> When comparing the type of an argument of a data constructor
> between the from-type and the to-type, we used to regard any appearance
> of any type_ctor other than the one on the left-hand-side of the
> type definition as requiring any type variable appearing in the type
> as having to be invariant.
> 
> This prevented coercions from e.g. one_or_more(err_spec) to
> one_or_more(diag_spec) despite err_spec being a subtype of diag_spec,
> because this rule applied to the  appearance of list(T) in the type of
> the second arg of the one_or_more data constructor.
> 
> Fix this situation by changing the code that did to allow it to
> test, while gathering the invariant type parameters of the one_or_more/1
> type_ctor, to invoke itself recursively to compute the set of invariant
> type parameters of list/1 as well. In general, we can now look
> arbitrarily deep into type structures, subject to some limitations.
> 
> - The first limitation is that we keep a stack of the type_ctors
>   whose sets of invariant type params is being computed, and use it
>   to return a conservative approximation of the actual result
>   instead of descending into infinite recursion.
> 
> - The second is that we do not keep track of any substitutions
>   of any type parameters. This means that for any type_ctor
>   other than the one at the top level, all we care about is
>   whether it may have any invariant type params; if it may,
>   then we treat *all* its type params as having to be invariant.

Please update the reference manual to reflect the rules.

> The extent, if any, to which these limitations are a problem
> in practice is something we can find out only via experience.
> 
> compiler/typecheck_coerce.m:
>     Make the change described above.
> 
> tests/invalid/coerce_type_error.err_exp:
>     Stop expecting an error message about coercing one_or_mores.
> 
> tests/invalid/coerce_typecheck_eqv_helper_1.m:
>     Change a type definition to give second/2 type_ctor some
>     type parameters that must be invariant. Without this change,
>     the compilation of the coerce_typecheck_eqv test case would have
>     succeeded, which means the test case would "fail to fail".
> 
> tests/invalid/coerce_typecheck_eqv.err_exp:
>     Expect the diagnostics appropriate to the changed test case.

> diff --git a/compiler/typecheck_coerce.m b/compiler/typecheck_coerce.m
> index 9b107f500..0a8370cc6 100644
> --- a/compiler/typecheck_coerce.m
> +++ b/compiler/typecheck_coerce.m
> @@ -138,8 +138,10 @@ typecheck_coerce_in_type_assign(Info, Context, FromVar, ToVar,
>          % NOTE The following block of code has a near-duplicate below
>          % in check_coerce_constraint_if_ready, though the two places differ
>          % in how they handle both resolved and not-yet-resolved constraints.
> +        % XXX Should we make known_type_ctors part of typecheck_info?
>          typecheck_coerce_between_types(TypeTable, TVarSet0,
> -            FromType, ToType, TypeAssign2, TypeAssign3, CoerceFails),
> +            FromType, ToType, TypeAssign2, TypeAssign3,
> +            init_known_type_ctors, _, CoerceFails),

Name the anonymous variable.

> @@ -290,7 +292,8 @@ check_coerce_constraint_if_ready(TypeTable, Coercion0, Action, !TypeAssign) :-
>              % in how they handle both resolved and not-yet-resolved
>              % constraints.
>              typecheck_coerce_between_types(TypeTable, TVarSet0,
> -                FromType, ToType, TypeAssign0, TypeAssign1, CoerceFails),
> +                FromType, ToType, TypeAssign0, TypeAssign1,
> +                init_known_type_ctors, _, CoerceFails),
>              (
>                  CoerceFails = [],
>                  type_assign_get_type_bindings(TypeAssign1, TypeBindings1),

Likewise.

> @@ -356,12 +359,31 @@ coerce_constraint_is_satisfied(Coercion) :-
>  % Part 3.
>  %
>  
> -:- pred typecheck_coerce_between_types(type_table::in, tvarset::in,
> -    mer_type::in, mer_type::in, type_assign::in, type_assign::out,
> +    % The type_ctors that we are currently trying to add to known_type_ctors.
> +    %
> +:- type active_type_ctors == list(type_ctor).
> +
> +    % The set of type_ctors for which we know whether they have any
> +    % type parameters which must be invariant.
> +    %
> +    % XXX A better name than known_type_ctors would be nice.
> +    % Maybe known_invariant_tparam_type_ctors, but it is kind of long.
> +    %
> +:- type known_type_ctors == map(type_ctor, invariant_tvars).
> +
> +:- func init_known_type_ctors = known_type_ctors.
> +
> +init_known_type_ctors = map.init.
> +

type_ctor_invariant_tvars_map or invariant_tvars_map?
Not very short, but more descriptive.

> diff --git a/tests/invalid/coerce_typecheck_eqv.err_exp b/tests/invalid/coerce_typecheck_eqv.err_exp
> index 6542293fe..73670338a 100644
> --- a/tests/invalid/coerce_typecheck_eqv.err_exp
> +++ b/tests/invalid/coerce_typecheck_eqv.err_exp
> @@ -1,3 +1,12 @@
> +coerce_typecheck_eqv.m:030: In clause for predicate `test1'/2:
> +coerce_typecheck_eqv.m:030:   error: cannot coerce `X' from `good(citrus)' to
> +coerce_typecheck_eqv.m:030:   `good(fruit)'.
> +coerce_typecheck_eqv.m:030:   The only parameter of the type constructor
> +coerce_typecheck_eqv.m:030:   `good'/1 must be invariant (meaning that it must
> +coerce_typecheck_eqv.m:030:   be bound to the same type in the coerced-from and
> +coerce_typecheck_eqv.m:030:   coerced-to types) because it occurs in the type
> +coerce_typecheck_eqv.m:030:   of the first argument of the `good'/1 data
> +coerce_typecheck_eqv.m:030:   constructor, which is an abstract type.
>  coerce_typecheck_eqv.m:039: In clause for predicate `test2'/2:
>  coerce_typecheck_eqv.m:039:   error: cannot coerce `X' from `bad(citrus)' to
>  coerce_typecheck_eqv.m:039:   `bad(fruit)'.
> @@ -6,5 +15,4 @@ coerce_typecheck_eqv.m:039:   `bad'/1 must be invariant (meani
>  coerce_typecheck_eqv.m:039:   be bound to the same type in the coerced-from and
>  coerce_typecheck_eqv.m:039:   coerced-to types) because it occurs in the type
>  coerce_typecheck_eqv.m:039:   of the first argument of the `bad'/1 data
> -coerce_typecheck_eqv.m:039:   constructor, which has a type constructor other
> -coerce_typecheck_eqv.m:039:   than `bad'/1, namely `list'/1.
> +coerce_typecheck_eqv.m:039:   constructor, which is an abstract type.

Both test1 and test2 are the same now, which is fine, but good/1
is now "bad" as well.

Actually, test1 should not have been accepted before. Since second(T, U)
is abstract exported, first(int, T) should only have been expanded out
to second(T, int), so T should be an invariant type parameter.
But I guess it is another case of the compiler expanding out
abstract equivalence types when it shouldn't do.

> diff --git a/tests/invalid/coerce_typecheck_eqv_helper_1.m b/tests/invalid/coerce_typecheck_eqv_helper_1.m
> index 868c180ff..4fe2e4b39 100644
> --- a/tests/invalid/coerce_typecheck_eqv_helper_1.m
> +++ b/tests/invalid/coerce_typecheck_eqv_helper_1.m
> @@ -13,4 +13,6 @@
>  
>  :- import_module list.
>  
> -:- type second(T, U) == list(U).
> +:- type second(T, U)
> +    --->    second_base
> +    ;       second_rec(T, second(U, T)).

You should update the comments in coerce_typecheck_eqv.m

Peter


More information about the reviews mailing list