#13388: local module type substitution can fail#13540
Conversation
Local substitutions for module types ``` module type t := sig end ``` or ``` s with module type t := sig end ``` may fail when trying to substitute the module type path inside the type of first class module. Before this commit, the `Typemod` logic was duplicating part of the substitution composition logic to detect those failing substitutions in advance but failed to detect many cases. This commit introduces an alternative (and easier to maintain) approach which splits substitutions in two types belonging to the same type family: - standard substitutions that are guaranteed to succeed - local substitutions that may fail With this change, `Typemod` can use local substitution without affecting the rest of the typechecker.
|
|
||
| val compose: local -> local -> local res | ||
| (** Due to the eager nature of composition for module types, composition of | ||
| local substitution may fail. *) |
There was a problem hiding this comment.
The way I read this is "this may be eager, and so it may fail". But as a user, I may want the guarantee that this is eager, so that it fails in my code when I expect it to fail. (The alternative in some cases is not to fail later, but to never fail because some ill-formed types appear during composition but not in the final result.)
|
The design looks nice and I feel much better about this approach. Silly question: what is a "local" substitution, why are those substitutions called local? |
|
The "local substitution" name is the one used in the manual for type t := int * floatto convey the point that those substitutions only exist inside the local scope of the module. I agree that the name doesn't completely fit in the context of the typechecker code. |
| (** Standard substitution*) | ||
|
|
||
| type local = [`local] s | ||
| (** Local substitution *) |
There was a problem hiding this comment.
Naming proposal: some substitutions are "safe" (they always preserve well-formedness of the objects they operate on), some are "unsafe" (they may introduce ill-formed terms, which are detected at runtime). (Gets rid of "local" here, which is good).
There was a problem hiding this comment.
Unsafe was the first name of those variants, I have reverted the interface to this naming scheme.
There was a problem hiding this comment.
On reading the file below, I find unsafe a bit unclear, I would prefer unsafe_subst or just [`Unsafe] subst. Maybe just type safe = [`Safe], and then safe subst?
There was a problem hiding this comment.
Nitpick: now you define type unsafe but there is no corresponding type safe alias, which could be slightly annoying for someone who would want to use this type explicitly. Could you introduce a safe type?
| Path.t -> params:type_expr list -> body:type_expr -> 'a s -> 'a s | ||
| val add_module: Ident.t -> Path.t -> 'a s -> 'a s | ||
| val add_module_path: Path.t -> Path.t -> 'a s -> 'a s | ||
| val add_modtype_id_to_path: Ident.t -> Path.t -> 'a s -> 'a s |
There was a problem hiding this comment.
Naming proposal: add_modtype : Ident.t -> Path.t -> 'a s -> 'a s. It is only safe to expand a module type to a path, so it makes to name this as the canonical substitution operation for module types, as we are doing for modules above.
There was a problem hiding this comment.
I have aligned those function names to the one used for modules.
|
Module substitutions are limited to path substitutions and this is one of the main difference between |
|
I think I would expect |
|
Moving all module type editing functions to the This introduces a small amount of "noise" in the current state because some checks for recursive modules also use Having a separate full modules sounds like an interesting avenue to investigate, at least if we can keep code duplication to a minimum. However, my opinion is that it is probably better to fix the current bug now, and explore this idea as a subsequent refactorisation later. |
| when two local substitutions are incompatible (for instance `module type | ||
| S:=sig end type t:=(module S)`) | ||
| (Florian Angeletti, report by Nailen Matschke, review by Gabriel Scherer) | ||
| (Florian Angeletti, report by Nailen Matschke, review by Gabriel Scherer, and |
There was a problem hiding this comment.
I would remove the oxford comma, we don't do usually that in the Changes file.
There was a problem hiding this comment.
The use of the oxford comma varies quite a lot within the Changes file.
| (** Standard substitution*) | ||
|
|
||
| type local = [`local] s | ||
| (** Local substitution *) |
There was a problem hiding this comment.
On reading the file below, I find unsafe a bit unclear, I would prefer unsafe_subst or just [`Unsafe] subst. Maybe just type safe = [`Safe], and then safe subst?
| val add_type_path: Path.t -> Path.t -> 'k subst -> 'k subst | ||
| val add_type_function: | ||
| Path.t -> params:type_expr list -> body:type_expr -> 'k subst -> 'k subst | ||
| val add_module_path: Path.t -> Path.t -> 'k subst -> 'k subst |
There was a problem hiding this comment.
I find it confusing that these functions are in Unsafe, but their type remains safety-polymorphic. Did you just forget to return unsafe, or is the idea that it would break too much code so you prefer to brand them in the submodule without changing their type for now, or forever?
There was a problem hiding this comment.
Oops, I forgot to change those types, the interesting point with this move is that it require very little changes.
Concerning, the name I would propose:
type unsafe = [`Unsafe]
...
module Unsafe: sig
type t = unsafe subst
...
endThere was a problem hiding this comment.
Yep, this is my favorite proposal as well.
gasche
left a comment
There was a problem hiding this comment.
I believe that the PR is correct and that it simplifies and improves the codebase -- besides fixing a bug. Approved.
| (** Standard substitution*) | ||
|
|
||
| type local = [`local] s | ||
| (** Local substitution *) |
There was a problem hiding this comment.
Nitpick: now you define type unsafe but there is no corresponding type safe alias, which could be slightly annoying for someone who would want to use this type explicitly. Could you introduce a safe type?
582fc50 to
0a01326
Compare
|
I have added the |
|
As a bug fix, I am planning to cherry-pick the fix to 5.3 . |
|
Cherry-picked to 5.3 (542549a) since this is a bug fix. |
Local substitutions for module types
or
may fail to produce well-formed types when trying to substitute a module type path inside the type of first class module, for instance
Before this PR, the part of
Typemodmodule handling the erasure of local substitution was tracking independently whenever a substitution could be applied safely or not on a type expression. However, this logic was incomplete and did not cover correctly the composition of substitutions.Rather than extending this logic as proposed in #13524, this PRs introduces an alternative (and easier to maintain) approach which splits substitutions from the
Substmodule in two variants belonging to the same type family and distinguished by a phantom type parameter:With this two types, the definition of substitution composition is defined in one place the
Substmodule.Local substitutions are created by adding a generic module type substitution to a substitution and
applying a local substitution require to handle the failure case.
With this change, the
Typemodmodule can use the new local substitutions to detect whenever a substitution fail to create a well-formed type.without affecting the rest of the typechecker.