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.
How would you mutate a string literal used in a constant pattern?
There was a problem hiding this comment.
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.
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.
(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.
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.
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).
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.
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.
Rather not Config.safe_string. We need the system to be configured with safe strings to be sure that strings cannot be mutated.
alainfrisch
left a comment
There was a problem hiding this comment.
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_descIndeed 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.
Shouldn't the not be removed here? When Config.safe_string, reads from strings are inactive.
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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. *) |
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.
Shouldn't this be List.for_all, = Immutable, &&?
There was a problem hiding this comment.
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.
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. |
Changes
Outdated
|
|
||
| - 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.
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.inactivedoes 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.