Expand aliases if necessary during immediacy computation (third attempt)#11841
Expand aliases if necessary during immediacy computation (third attempt)#11841ccasin wants to merge 10 commits intoocaml:trunkfrom
Conversation
|
Thanks! I am on vacation right now (until January 3rd) but I hope to look at this shortly. |
gasche
left a comment
There was a problem hiding this comment.
I started reviewing this PR, and I realize that the first commit does not build. If you think that this should be best reviewed commit-by-commit (I think it was initially intended this way), then it would be nice to ensure that each commit builds separately. Or should we rather look at the whole diff at once?
|
To quickly answer the question about how to review: I debated whether to preserve the multi-commit structure of Stephen's original PR (which also had the property that the first commit didn't build). It's unfortunately tricky to make that commit have sensible behavior, because the boring changes are plumbing related to removing the old immediacy system and adding the spot for immediacy information in I decided to keep the split because there are really quite a lot of these "boring" changes, and isolating the interesting parts into the second commit felt possibly useful, but feel free to review it all at once if that's easier. |
gasche
left a comment
There was a problem hiding this comment.
I reviewed the third commit (refactoring), and I agree it is an improvement. See minor inline comments. (Now I have the second commit left to look at.)
gasche
left a comment
There was a problem hiding this comment.
I have now reviewed the "substantial changes" commit -- the last commit I hadn't reviewed yet. See inline comments.
Reading this code again reminded me that I would prefer to change the immediacy lattice to include the "not immediate" case, which would clarify the code (and optimize the computation in some corner cases). I talk a bit about it in my review comments, but I consider this as future work, not a requested change in the context of the present PR.
| | Type_variant (cstrs, _) -> | ||
| if List.exists (fun c -> c.Types.cd_args <> Types.Cstr_tuple []) cstrs | ||
| then Type_immediacy.Unknown | ||
| else Type_immediacy.Always |
There was a problem hiding this comment.
(This is a "thinking out loud" remark with no actual change suggestions for the present PR, apologies for the noise.)
In this function I find the use of Type_immediacy.Unknown perplexing, because that immediacy is supposed to mean "we don't know whether the type is immediate or not" (it is the immediacy of an abstract type with no immediacy annotation on it), but we use it here for types that we know are not immediate. I would rather expect a Type_immediacy.Never value in this case.
I vaguely remember that I proposed such a "not immediate" immediacy to @stedolan in one of the earlier attempts (in the context of a fairly different approach to computing and checking immediacies), and that he was not convinced. I still think that it would be more natural than the code we write here -- or in other parts of the immediacy computation.
On the other hand, having this should not be a requirement for the present PR, whose scope is to make the computation of immediacy "on-demand" rather than store it for each type. I am not suggesting that it must be added now, I think it could be something to consider for another PR.
Currently immediacies form a lattice
Immediate <= Immediate64 <= Unknown
and we would have instead
Immediate <= Immediate64 <= Unknown
NonImmediate <= Unknown
There was a problem hiding this comment.
I agree it would great to have a way to record that types are never immediate.
In addition to clarifying this code, it could be a performance win - as you observed below, the immediacy recorded in type kinds is an upper bound, so when we see Type_immediacy.Unknown on a type abbreviation we sometimes expand the manifest to check whether the upper bound was conservative. This is potentially expensive, and could be skipped if we had a way to record types were NonImmediate even after substitutions. We've been considering different designs for this in the context of our "unboxed types" prototype (which essentially just expands the lattice you suggest with still more options - different "layouts"), but haven't settled on anything and plan to revisit it in the future.
| | Ok () -> Ok () | ||
| | Error _ -> | ||
| let ty = get_unboxed_type_representation env ty in | ||
| Type_immediacy.coerce (type_immediacy_head env ty) ~as_:imm |
There was a problem hiding this comment.
I find the logic here a bit strange. One may think of type_immediacy_head as computing the immediacy of a type constructor, but its result is actually incorrect on unboxed types, which are not taken into account in kind_immediacy. So if we get a failure, we try again after calling `get_unboxed_type_represeentation.
Two questions:
- Why not always get the unboxed type representation first? Performance reasons, I suppose: we want to avoid expanding abbreviations, because abbreviations already carry correct immediacy information in their kind.
- Why not have
kind_immediacyhandle unboxed datatypes, so thattype_immediacy_headreturns the correct result for all types? Would we still need a fallback in this case? What would be the downside?
There was a problem hiding this comment.
Why not always get the unboxed type representation first?
You are exactly correct - this is for performance reasons. get_unboxed_type_representation is potentially expensive, so in the case of a Tconstr we first look at the kind and see if it yields a definitive answer. If not, we perform the expensive expansion and unboxing.
This optimization was already present in the previous version of this PR, but it was implemented by a recursive call in check_type_immediate in the case we needed to call get_unboxed_type_representation. That approach was somewhat confusing, so here I've incorporated a suggestion from @lpw25 from that PR to eliminate the recursion by breaking it into two functions (check_type_immediate and type_immediacy_head).
The intent, then, is that type_immediacy_head is a cheap look at the "head" of the type, which check_type_immediate tries and then calls get_unboxed_type_representation if it doesn't provide a definitive answer.
Why not have
kind_immediacyhandle unboxed datatypes, so that type_immediacy_head returns the correct result for all types?
kind_immediacy may return an approximation in two cases: unboxed types, and abbreviations (because the immediacy recorded on Type_abstract is a conservative upper bound, as you observe elsewhere). As the fallback in both cases is the same (check_type_immediate calls get_unboxed_type_representation), it seemed cleanest to leave the unboxing logic there.
An alternative design would be to duplicate more of the logic of get_unboxed_type_representation here. In particular, one advantage would be that we could check whether we have a definitive immediacy after each recursive step of expansion and unboxing. But as this function and its fuel parameter are somewhat unfortunate, and it sounds like there are medium term plans to replace it, my instinct is not to do this.
So, to summarize, I do think we need some fallback in any event to deal with abbreviations, and it therefore seems cleanest not to complicate kind_immediacy. But let me know if you disagree. And in any event the situation should be documented - I'll add a comment if that sounds reasonable.
There was a problem hiding this comment.
I find the current state disturbing: there is a notion of "the immediacy of a kind", which would be an inexact approximation of "the immediacy of the type", but it is not what kind_immediacy currently computes. But it is not so simple to compute the "right" thing (as I suggested earlier) because it the case of unboxed datatypes we would need to recursively call type_immediacy_head.
In the short term, I thing maybe you could just add a comment to point out that kind_immediacy is incorrect on unboxed datatypes.
There was a problem hiding this comment.
I have changed the name to kind_immediacy_approx and added a comment to the mli.
|
|
||
| and ('lbl, 'cstr) type_kind = | ||
| Type_abstract | ||
| Type_abstract of {immediate : Type_immediacy.t} |
There was a problem hiding this comment.
Note: there is a non-obvious design choice being made here, which is that abbreviations (with use Typ_abstract kind with a Some ty manifest) will carry an immediacy. One may think that it is not necessary to store an immediacy for abbreviation as it can always be computed on-demand from the definition, and only "truly abstract" types without a manifest need an immediacy. (If we wanted to go "full on-demand" we would consider that.) The current state is a compromise, which is fine with me. It may also work better in presence of type constructors that are not fully abstract but whose definition is not in the environment / has been forgotten.
But: if I understand correctly, sometimes the information in this 'immediate' field may be stale / incorrect / a pessimistic approximation. (Otherwise we would not need the fallback logic in this case in check_decl_immediate.) I think that this happens in the functor-instantation scenarios that were discussed in the previous iterations of the PR, where currently the computation of variance and other type properties is not redone after substitution.
I think this should be documented: the immediacy information in the kind here may be an imprecise approximation of the immediacy of the manifest (when available), which is the "true" immediacy of the type.
There was a problem hiding this comment.
Yes. I think we can end up in the conservative approximation situation even without functors (e.g., type 'a t = 'a is not immediate, but int t is). I've added a comment mentioning this and directing readers to the original motivating examples for details, but let me know if it's not quite what you wanted.
typing/ctype.ml
Outdated
| | Type_variant ([{cd_args = Cstr_tuple [arg]; _}], Variant_unboxed) | ||
| | Type_variant ([{cd_args = Cstr_record [{ld_type = arg; _}]; _}], | ||
| Variant_unboxed)) } -> | ||
| check_type_immediate env arg imm |
There was a problem hiding this comment.
here again, we would not need this case if kind_immediacy worked correctly with unboxed datatypes, right?
There was a problem hiding this comment.
Agreed - I guess I'd say there's a tradeoff, either we perform this check here, or check_type_immediate will potentially perform it twice (once in kind_immediacy and again when we fall back to get_unboxed_type_representation).
typing/typedecl.ml
Outdated
| begin match Type_immediacy.coerce kind_imm ~as_:immediate with | ||
| | Ok () -> () | ||
| | Error v -> raise(Error(sdecl.ptype_loc, Immediacy v)) | ||
| end; |
There was a problem hiding this comment.
One awkward thing about this function is that there is code to "check" the declaration error cases in three distinct places:
- the one for the unboxed attribute above
- the one for the immediacy attribute here
- the one for constraints below
Case (3) is fairly different in nature, but for cases (1) and (2) it is annoying that there is no common structure to the placement: (1) is placed right when the attribute is accessed, while (2) is placed after a bunch of non-trivial logic. There is an explanation (the check (1) is self-contained from the information already there, the check (2) requires building more of the declaration to make sense), but the code remains slightly inconsistent. If I was adding support for yet another kind of attribute, where would I put the checking code? "In a random place that works" is what the current code says.
My suggestion:
- move the logic of both checks (1) and (2) to auxiliary functions,
verify_unboxed_attrandverify_immediacy_attror something - call both those functions at the same place in the code, for example at the current point (2) or after the
let decl = ... indeclaration.
I think this would make the code both more readable and easier to extend.
There was a problem hiding this comment.
Agreed - that does sound better. I'll take a stab at this tomorrow.
There was a problem hiding this comment.
I made the change you suggested. However, while doing so, I realized there remains an issue. In particular, this PR will reject the following declaration which is accepted by current ocaml.
type t = { x : int } [@@unboxed] [@@immediate]
This behavior is present in the previous iteration of this PR (#10041) but I suppose was not noticed at that time. As the issue is that kind_immediacy does not traverse through @@unboxed, I feel this somewhat vindicates your unhappiness with that function :).
I can think of a few solutions. The most obvious is to rename kind_immediacy to something like kind_immediacy_approx, and write a new kind_immediacy that does unbox things. I think this new function would need to call get_unboxed_type_representation if it sees an unboxed type - unboxing just one level is potentially insufficient. And it's possible the check needs to be moved later in transl_type_decl to accommodate mutually recursive or circular types (but I haven't thought that through yet). But I thought I'd get your opinion before implementing something.
There was a problem hiding this comment.
Ah, good catch! (If you haven't done this yet, you should add the example to the testsuite.)
I agree with your suggestion to rename kind_immediacy intokind_immediacy_approx to make things more obvious. This will encourage us to consider this issue for each callsites, which we initially forgot to do ourselves.
Regarding how to fix this: my naive reaction is that we were wrong to call only kind_immediacy here and should have called the proper immediacy-checking function that uses the backtracking behavior, check_decl_immediate. Is there a reason that we cannot do this at this step, on the decl below? (Maybe the declaration is not fully-formed or not already in the typing environment or something, or we end up trusting the annotation before checking it?)
There was a problem hiding this comment.
OK, I've pushed a possible fix. Here's my thinking:
We can not perform the kind check in transl_declaration. The issue is mutually defined types like:
type t = { x : s } [@@unboxed] [@@immediate]
and s = int
Checking that t's kind is compatible with the [@@immediate] attribute requires access to s's manifest, but the environment used for transl_declaration doesn't have accurate manifests for mutually defined types. This declaration is accepted by today's OCaml, so we don't want to reject it.
So we must perform this later - after the call to update_type in transl_type_decl. It seems sensible to put it with the other checks in that function after the ill-formed abbrevs check.
I've done that now. So in transl_type_decl we simply copy the immediacy attribute into the kind (for the abstract case), and we check it later. Here I describe why I think the check I've added is sufficient, in combination with check_coherence:
- We might have an
[@@unboxed]type, in which case we callcheck_type_immediateon the inner thing. This inner thing might be an mutually defined abstract type whose annotation we have trusted, but in that case its manifest is checked bycheck_coherencelater. - Otherwise, we just use
kind_immediacy_approx. The two cases where this is innaccurate are unboxed types (already handled) and abstract types, which are either purely abstract (fine) or abbreviations (checked bycheck_coherence).
The last question is whether check_coherence itself can be trusted. In particular, it checks that the manifests of abbreviations marked [@@immediate] are actually immediate, and the manifest may mention other abbreviation from the mutually recursive group. But this is fine, because their manifests will also be checked. And if there is a cycle, it's fine to treat all of them as immediate.
One consequence is that this declaration is allowed by this PR but is rejected by current OCaml:
type t = { x : s } [@@unboxed] [@@immediate]
and s = t [@@immediate]
This seems fine. Unfortunately, this similar declaration is not allowed, and could be:
type t = { x : s } [@@unboxed] [@@immediate]
and s = t
The difference is that in the former case s's kind records it is immediate, hitting the fast path of immediacy checking, while in the latter case we must expand, which runs out of fuel. This seems acceptable, if unfortunate, to me. But we can revisit the design if you disagree (I think it can be fixed, but at some cost in the complexity of the immediacy checking code, which doesn't seem worth it for such a silly type).
| if !Clflags.native_code && Sys.word_size = 64 then Always_on_64bits | ||
| else Always | ||
| in | ||
| Result.is_ok (Ctype.check_type_immediate env ty imm) |
There was a problem hiding this comment.
I find the new definition harder to read than before. It comes from previous design iterations of this PR, which had functions to check whether a given type satisfied a given immediacy, but no function to actually compute the immediacy of a type. But this is not the case anymore. Could we write the following?
let is_immediate env ty =
match Ctype.(type_immediacy_head env ty) with
...?
(I think this would require tuning type_immediacy to work correctly on unboxed types, as suggested in another comment.)
There was a problem hiding this comment.
Interesting! I found the old version confusing (particularly, is_immediate seemed an odd name for a Type_immediacy.t -> bool).
I think we still do not have a function to calculate the exact immediacy - as described above the issue is not just unboxed types but also the approximation in the kind for abbreviations. We could add such a function - an analogue of check_type_immediate that returns the immediacy. My instinct is that this would be unnecessarily duplicative if it's only used in this one place, but I'm open to other opinions.
|
We are at a state where I think that the implementation is basically fine, but I still dislike some aspects of the design of the PR, which is that it defines a notion of immediacy without providing a function to compute the immediacy of a type. (This is not new, it was already a criticism of previous iterations.) I think that computing the immediacy of a type would be the better design, but we would need to add a I think that we could move forward by merging the current PR with the current interface that I don't really like, and maybe I could propose a follow-up PR to implement my more invasive suggestion. @ccasin, what do you think? (In that scenario, would you be available to review a follow-up PR?) |
I'm not quite sure adding
Yes, I would be happy to commit to reviewing a follow-up PR if we can find an acceptable solution to the remaining issue in this one. |
After thinking about this, I think that you are right, but that this can only happen in a corner case: when we are checking Other cases:
Thoughts:
|
I think a key point here is that this corner case quickly stops being a corner case once you expand the lattice to general kinds. Then you really do want to use a check function rather than a two step "get me the best answer, then compare it" approach. I think this is generally the correct approach in most subtyping-style systems: it is usually the case that many checks can be short-circuited without finding out the most accurate information. |
|
(I also just realized that I missed you make the exact same point -- so just consider the above a +1 to what you said in your last bullet). |
|
Yes. I still wonder whether it would be possible to present the computation as a "compute the property then compare", without loss in efficiency, by using a different interface, for example by returning an on-demand stream of increasingly-precise approximations. This is just a conceptual distinction, outside the scope of the present PR. That approach (compute then compare) is the one that relates most clearly the implementation of the check with the semantics of the property. |
|
I was just looking back at this and realized I had forgotten to actually push the fix I described in my last comment - sorry, I've done that now. I think where this stands is that we've agreed on everything except a fix for the problem identified with unboxed types - the comment here describes the fix I've just pushed (but meant to push last week). |
|
Quoting the "comment above" from @ccasin to discuss this important question in the main thread.
|
|
@ccasin I apologize for the silence here; I was at a conference last week and am trying to write a paper this week. I won't be back to reviewing this in details before next week. I understand the high-level idea of the approach you describe and I agree that it sounds okay. |
|
I understand that there have been significant changes to this design on the Jane Street side, see in particular oxcaml/oxcaml#1384 . When they have time, I would be curious to hear from @goldfirere or @ccasin on their current understanding, and what is the best approach moving forward. |
Sorry for the slow response - I was on vacation last week - and for not getting back to this sooner in general. You're right that things have moved a bit on our end. The main change is the one you linked - moving immediacy information out of the Abstract case of I am happy to rebase this PR and make that change (though likely not for a week or two). Would it be easier for you if I pushed those changes as new commits here, or closed this and opened a fresh PR when ready? |
|
There is no hurry on my end (next week it will be my turn to go for vacations for the month of August), but I would still be interested in getting something close to your final design, whatever it is, upstreamed. Regarding whether to update here or open a new PR, please do as you prefer -- when the time is right. |
|
My understanding is that the Jane Street type-checker is not using this approach anymore, so my hope of merging this to reduce the diff before the two is not up-to-date, and the best thing to do is to close the current PR -- with apologies for @ccasin for the time investment on his part... It was a pleasure trying to get into this again. |
Agreed. The newer approach is not very different, and I do still want to make a PR here with it, but it's fine to close this and do that as a separate PR considering that I've clearly failed to find time for this. Thanks! |
This is a rebased version of @stedolan's #10041 (which itself was an alternate approach to #10017 - good discussions in both of those PRs).
To copy the original description: "The major change is to entirely remove the fixpoint computation in typedecl_immediacy.ml which computed the type_immediacy field of each named type. Instead, the immediacy of abstract types is represented as a parameter to the Type_abstract kind. Immediacy is checked when the type is defined rather than being inferred.
Removing the type_immediacy field fixes the bugs in #10017: the bugs arise because the contents of this field becomes stale after substitutions."
Following @stedolan's approach, this is broken into a few commits. The first is boring mechanical changes. The second has the main substantive changes. The third is a minor piece of related cleanup that seemed appropriate here (but could be deleted if it's undesirable for any reason).
Looking back at the discussion in #10041, I think there were two main pieces of remaining feedback:
check_type_immediatecould be clarified by splitting it into two functions and eliminating the recursion. That's done here (folded into the second commit).get_unboxed_type_representationandtry_expand_once_opt. This might be an improvement, but seems somewhat orthogonal to the main purpose of this PR, so I suggest investigating it separately.(cc @gasche - sorry it took me so long to get to this)