Skip to content

Fix resolution involving deeply nested substitutions #727

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 14, 2021

Conversation

jonludlam
Copy link
Member

Fixes #690

There has been logic to fix this sort of issue for a long time, but it
broke at some point.

Mostly the fix is that when calculating the signatures of With
module expressions, we always recalculate rather than using any
previously calculated signature - this is because the previously
calculated signature makes a round-trip from Component.* to Lang.* and
Lang.ModuleType.simple_signature doesn't preserve removed items as the
Components layer does.

By itself this would not have fixed #690 though - The way the rest of
the fix works is by ensuring that at each level of the expansion we
have enough to figure out that a replacement is necessary and what it
should be. For example, in issue #690, @Octachron provides the
following example:

module type S = sig
  module M: sig
    type t
  end
  type t = M.t
end
module type T = S with type M.t := int

Before this fix, the expansion of T would look like this:

module M : sig end
type t = M.t

Clearly the M.t can't be resolved from this. Instead, this fix
works by ensuring that the expansion of T looks like this:

module M: sig type t end with type t := int
type t = M.t

From this we have enough information to replace the M.t with an
int at the point we do the resolution of the RHS of type t.

We don't really want the output to show
sig type t end with type t := int however, so in the document
layer these expressions are replaced with sig ... end as we had
before.

Fixes ocaml#690

There has been logic to fix this sort of issue for a long time, but it
broke at some point.

Mostly the fix is that when calculating the signatures of `With`
module expressions, we always recalculate rather than using any
previously calculated signature - this is because the previously
calculated signature makes a round-trip from Component.* to Lang.* and
Lang.ModuleType.simple_signature doesn't preserve removed items as the
Components layer does.

By itself this would not have fixed ocaml#690 though - The way the rest of
the fix works is by ensuring that at each level of the expansion we
have enough to figure out that a replacement is necessary and what it
should be. For example, in issue ocaml#690, @Octachron provides the
following example:

```ocaml
module type S = sig
  module M: sig
    type t
  end
  type t = M.t
end
module type T = S with type M.t := int
```

Before this fix, the expansion of `T` would look like this:

```ocaml
module M : sig end
type t = M.t
```

Clearly the `M.t` can't be resolved from this. Instead, this fix
works by ensuring that the expansion of `T` looks like this:

```ocaml
module M: sig type t end with type t := int
type t = M.t
```

From this we have enough information to replace the `M.t` with an
`int` at the point we do the resolution of the RHS of type `t`.

We don't really want the output to show
`sig type t end with type t := int` however, so in the document
layer these expressions are replaced with `sig ... end` as we had
before.
@jonludlam jonludlam force-pushed the fix-deep-substitution branch from ff678fd to c670199 Compare September 13, 2021 10:46
Comment on lines +18 to +21
$ ocamlc -c -bin-annot m.mli
$ odoc compile m.cmti
$ odoc link m.odoc
$ odoc html-generate m.odocl --indent -o .
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a helper script for this:

Suggested change
$ ocamlc -c -bin-annot m.mli
$ odoc compile m.cmti
$ odoc link m.odoc
$ odoc html-generate m.odocl --indent -o .
$ compile m.mli

The output is slightly different because it sets the parent. If that's annoying, perhaps we can change the script and update every other tests ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have a helper script, true - but this way the test is a bit more self contained, and I often find this useful when I'm working on them because I find building them out of tree to be essential when developing (and debugging) these tests.

@@ -1430,14 +1430,22 @@ module Make (Syntax : SYNTAX) = struct
++ Link.from_path (m :> Paths.Path.t)
++ O.txt " " ++ O.keyword "end"

and is_elidable_with_u : Odoc_model.Lang.ModuleType.U.expr -> bool = function
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not render nested substitutions ? It is rendered for T:

# Module Test
module type S = sig ... end
module type T = S with type M.t := int

but the documentation for T doesn't mention any substitution at all:

# Module type Test.T
module M : sig ... end
type t = int

Showing the substitution is not that bad:

# Module type Test.T
module M : M.S with type t := int
type t = int

It would be even more useful in case of non-destructive substitution because the definition of t would be in the same documentation page:

# Module type Test.T
module M : M.S with type t = int
type t = M.t

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it was certainly not my intention to remove the 'with type' always, just the construct sig ... end with type ...

I'll double check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I misinterpreted what you mean - though I'm still not quite sure! To be clear, we've got:

module type S = sig
  module M: sig
    type t
  end
  type t = M.t
end
module type T = S with type M.t := int

You wrote that you thought this would be better:

# Module type Test.T
module M : M.S with type t := int
type t = int

I don't think you mean M.S - I think you might have meant S.M - but we can't do this because we can't have paths to elements within module types. Is this correct?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By M.S, I meant S.M or even Test.S.M, sorry for the confusion.

Hmm, it was certainly not my intention to remove the 'with type' always, just the construct sig ... end with type ...

sig ... end with type ... is turned into just sig ... end but I'd expect Test.S.M with type ... even if M is not a module type.

but we can't do this because we can't have paths to elements within module types.

On the definition of T, we have the path to S and the substitution, why can't nested substitution be represented the same way ?
Perhaps we can represent it like (module type of Test.S.M) with type ... internally ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem isn't M, it's S - S is a module type so we can't do S.M.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That wouldn't be valid OCaml but it would be a valid reference (so can work internally). Do we require that every rendered paths should be valid OCaml paths ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even internally it's a Path not a Reference, and in any case I strongly prefer sticking to valid OCaml than to deviate. If the language supports it some day we can do it - for example I'm keeping an eye on the transparent ascription PR as that will help with some tricker parts of handling module type of.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright :) Sorry for the digression.

@jonludlam jonludlam merged commit 1378e8a into ocaml:master Sep 14, 2021
jonludlam added a commit to jonludlam/opam-repository that referenced this pull request Oct 5, 2021
CHANGES:

Breaking changes
- Remove odoc-parser into a separate repository (@jonludlam, ocaml/odoc#700)

Additions
- OCaml 4.13 support (@Octachron, ocaml/odoc#687, ocaml/odoc#689)
- Better errors/warnings (@Julow, ocaml/odoc#692, ocaml/odoc#717, ocaml/odoc#720, ocaml/odoc#732)
- ModuleType 'Alias' support (@jonludlam, ocaml/odoc#703)
- Improved test suite (@lubega-simon, ocaml/odoc#697)
- Improved documentation (@lubega-simon, @jonludlam, ocaml/odoc#702, ocaml/odoc#733)
- Strengthen module types (@jonludlam, ocaml/odoc#731)

Bugs fixed
- `uwt` now can be documented (@jonludlam, ocaml/odoc#708)
- Fix resolution involving deeply nested substitutions (@jonludlam, ocaml/odoc#727)
- Fix off-by-one error in error reporting (@asavahista, ocaml/odoc#736)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dead references after deep destructive substitutions
2 participants