-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Only treat pure patterns as inactive #1308
Conversation
typing/parmatch.ml
Outdated
| Tpat_tuple ps | Tpat_construct (_, _, ps) | Tpat_array ps -> | ||
| Tpat_constant c -> begin | ||
match c with | ||
| Const_string _ -> false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would you mutate a string literal used in a constant pattern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could imagine:
let f ("foo" as c) = fun () -> c;;
let s = "foo";;
let g = f "foo";;
s.[0] <- 'g';;
g ()
Although in this case the functions won't be merged as the match is not (and cannot be) exhaustive.
Still, the code "feels more correct".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, sorry, the problem is of course mutating the matched value, not the pattern.
Could you adapt to take into account Config.safe_string? (I understand this is a bit useless since string literal patterns are not exhaustive.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I have a feeling that what I wrote makes no sense at all, oh well...)
@@ -0,0 +1,2 @@ | |||
10 | |||
10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Off-topic question for @gasche (somehow I'm guessing you're the one who cares/knows about this sort of things) is there a reason why the content of this directory has not been switched to expect tests? Do we actually prefer expect tests nowadays or ..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the expect tests currently only work for checking the output of the compiler itself (inferred types, warnings/errors, etc.), not the dynamic output of the compiled programs as is done here. We could try to generalize / also implement this. (I thought at first that this may be subsumed by ocamltest in #681, but in fact this is orthogonal, ocamltest is a test driver in OCaml but for this kind of tests it still uses .reference
files).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks!
typing/parmatch.ml
Outdated
| Tpat_tuple ps | Tpat_construct (_, _, ps) | Tpat_array ps -> | ||
| Tpat_constant c -> begin | ||
match c with | ||
| Const_string _ -> false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed this off-line but I'm putting it here so it's not forgotten: false
should be changed to Clflags.unsafe_string
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather not Config.safe_string
. We need the system to be configured with safe strings to be sure that strings cannot be mutated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change looks correct to me.
This could potentially lead to performance degradation (allocating intermediate closures for functions with multiple arguments, some of them extracting the value of mutable fields), but it's better to be on the safe side, and people can always write their code to keep the currying compilation scheme. One could consider reporting a warning when currying is broken because of the change, but I don't think it is worth the effort.
So good to merge, after the tweak using Config.safe_string.
I'm not convinced by this change.
The current PR overloads the name with something that becomes false when reading mutable state (arrays or mutable records), apparently getting closer to a notion of "pure" pattern, but it does not update the documentation comment nor explain why the change of semantics is desirable in general. I don't have a full understanding of the issue, but I have the impression that an optimization was wrong to rely on only this notion of "inactivity" (it needs a stricter check with different semantics), but that you fixed the symptoms by changing the definition instead of changing the optimization. |
The |
I looked at the story of this code in more details:
Some remarks:
|
This is not at all how I read the current documentation. I quote: (* An inactive pattern is a pattern whose matching needs only
trivial computations (tag/equality tests).
Patterns containing (lazy _) subpatterns are active. *)
let rec inactive pat = match pat with
| Tpat_lazy _ ->
false
| Tpat_any | Tpat_var _ | Tpat_constant _ | Tpat_variant (_, None, _) ->
true
| Tpat_tuple ps | Tpat_construct (_, _, ps) | Tpat_array ps ->
List.for_all (fun p -> inactive p.pat_desc) ps
| Tpat_alias (p,_,_) | Tpat_variant (_, Some p, _) ->
inactive p.pat_desc
| Tpat_record (ldps,_) ->
List.exists (fun (_, _, p) -> inactive p.pat_desc) ldps
| Tpat_or (p,q,_) ->
inactive p.pat_desc && inactive q.pat_desc
(* A `fluid' pattern is both irrefutable and inactive *)
let fluid pat = irrefutable pat && inactive pat.pat_desc Indeed the notion of "trivial" computation is not defined, but I don't see how one would assume that reading from an immutable block is "trivial" while reading from a mutable block is "not trivial". The implementation and the documentation both only mention forcing a lazy subpattern as the only source of "non-trivial computation", making it pretty clear to me that the intent is "executing OCaml code" versus "only accessing already-evaluated values". (By the way, Alain would chime in to point out that re-boxing unboxed floating-point values on the fly may execute arbitrary OCaml code if the GC kicks in, but let's ignore that issue for now, just as it is ignored in the rest of the pattern-matching machinery.) |
Well, personally I would never use "trivial" to describe a computation that does side-effects. Still I take your point about the documentation. I'll change it to something more explicit.
Maybe I'm missing something, but I don't see how that is relevant here. We're only concerned here with not pushing the pattern's side-effects under a lambda. |
My point is that you are silently changing the intent of the
The point was that reading a floating-point number from a pure (unboxed) record may execute code, and thus that curryfication, by delaying this read (and the underlying allocation), changes the observable behavior of the code. Apologies for the distraction. |
I really don't see how the old definition was any more well defined than the new one, but I've replaced "needs only trivial computations" with "does not perform side-effects". |
typing/parmatch.ml
Outdated
true | ||
| Tpat_constant c -> begin | ||
match c with | ||
| Const_string _ -> not Config.safe_string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the not
be removed here? When Config.safe_string
, reads from strings are inactive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. Well spotted. Fixed.
bytecomp/translcore.ml
Outdated
partial = partial'; }} as exp}] | ||
when Parmatch.fluid pat -> | ||
partial = partial'; }} as exp}], Total | ||
when Parmatch.inactive pat -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you decide to change the way (ir)refutability/partiality is checked in this test? (I'm fine with moving away with the fairly unclear name "fluid".) Is there a difference in meaning between Parmatch.irrefutable
and this new test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cf Leo's note in the PR description:
To try and avoid conflicts I've made it on top of #1195, so when reviewing this change only the second commit is relevant.
typing/parmatch.ml
Outdated
(* A `fluid' pattern is both irrefutable and inactive *) | ||
|
||
let fluid pat = irrefutable pat && inactive pat.pat_desc | ||
(* An inactive pattern is a pattern which does not perform side-effects. Patterns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does not perform side-effects: matching against it may be duplicated, erased or delayed without change in observable behavior of the program. (I think that pure
would also be a fine name, but inactive
is acceptable.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we are discussing the documentation side, I'd rather move the description of the function to the interface.
I'd also avoid the reference to "performing side-effects" (and the notion of "pure"), since this can have different meaning depending on the context. @gasche explanation seems to correspond closely to what it needed where the function is used, which is good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I've moved the comment to the .mli
and used @gasche's description rather than "performing side-effects".
Curryfication can always change the allocation behavior, and interleaving of effects between threads and from finalizer codes is not specified. So I don't think there is any issue here with the possibility of triggering the GC because of boxing, since the pattern is irrefutable. (The problem is for pattern-matching, where the "asynchronous" effects can break invariants assumed by the pattern matching compiler and lead to unsound code.) |
typing/parmatch.ml
Outdated
|
||
let fluid pat = irrefutable pat && inactive pat.pat_desc | ||
(* An inactive pattern is a pattern which does not perform side-effects. Patterns | ||
containing (lazy _) subpatterns or reads of immutable fields are active. *) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/immutable/mutable/
typing/parmatch.ml
Outdated
| Tpat_alias (p,_,_) | Tpat_variant (_, Some p, _) -> | ||
inactive p | ||
| Tpat_record (ldps,_) -> | ||
List.exists (fun (_, lbl, p) -> lbl.lbl_mut = Mutable || inactive p) ldps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be List.for_all
, = Immutable
, &&
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think you're right. One day someone will invent a type system that saves me from having to think so carefully about which way around logical operators need to be.
I apologize for the final nitpicking, but the current definition of
I think (2) would be the nicest choice. |
r := 20;; | ||
|
||
print_endline (string_of_int (h ()));; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to extend the test to cover the case of a record with some mutable and some immutable fields.
@lpw25 : could you please address @gasche 's nitpicking and @alainfrisch 's suggestion for an extra test? I think this PR should be part of 4.06, but for this to happen you need to act quickly. |
0cc6daf
to
2fdf89c
Compare
2fdf89c
to
69e13c5
Compare
Ok, nitpicking attended too, branch rebased, and change entry added. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if "Cousin nitpick" is a better character than "Old Scrooge" ;-) but for the record I think this PR is now essentially perfect and would encourage its merge -- after the typo fix, heh.
Changes
Outdated
@@ -73,6 +73,10 @@ Working version | |||
stubs. | |||
(Mark Shinwell, review by Pierre Chambart and Leo White) | |||
|
|||
- GPR#1195: Merge functions based on partiality rather than | |||
Parmatch.irrefutable. | |||
(Leo White, revuew by Thomas Refis and Gabriel Scherer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While we are at it, you should add Alain in the review list, even if his first round of review was a bit too enthusiastic.
69e13c5
to
5b2476b
Compare
Is the following behaviour intended (i.e. is the pattern supposed to be evaluated before all arguments of # let f {contents = c} () = c;;
val f : 'a ref -> unit -> 'a = <fun>
# let r = ref 10;;
val r : int ref = {contents = 10}
# let g = f r;;
val g : unit -> int = <fun>
# g ();;
- : int = 10
# r := 20;;
- : unit = ()
# g ();;
- : int = 10 |
Yes, the semantics of |
Parmatch.inactive
does not treat patterns that read mutable variables as active. This leads to the following bug:because the read of the reference has been pushed underneath the
fun () ->
.This PR fixes this by treating reads of mutable fields as active patterns. To try and avoid conflicts I've made it on top of #1195, so when reviewing this change only the second commit is relevant.