Tweak for evaluation order of labelled partial applications#10653
Tweak for evaluation order of labelled partial applications#10653lthls wants to merge 1 commit intoocaml:trunkfrom
Conversation
|
This looks correct to me. I think we probably need @garrigue's input since he wrote the surprising behaviour when all previous arguments were optional. |
|
@garrigue Any opinion on this? Otherwise I'm going to merge soon. |
lambda/translcore.ml
Outdated
| let body = | ||
| match build_apply handle ((Lvar id_arg, optional)::args') l with | ||
| match build_apply handle [Lvar id_arg, optional] l with | ||
| Lfunction{kind = Curried; params = ids; return; |
There was a problem hiding this comment.
I think that build_apply can now only return Lfunction with an empty list for args. That means that this branch and the next are no longer reachable.
There was a problem hiding this comment.
Are you sure ? I would expect this case to be reached when compiling e.g. let f ~x ~y ~z = () in f ~z:foo (which should generate more or less let z = foo in fun ~x ~y -> f ~x ~y ~z)
There was a problem hiding this comment.
I think so. Your example needs to generate the equivalent of:
let z = foo in
fun ~x ->
let f' = f ~x in
fun ~y ->
f' ~x ~y ~zin case there are side-effects in f after the x is applied.
There was a problem hiding this comment.
Ah, you're right. I'll remove the unused cases. This code was building curried functions without checking the max_arity setting anyway.
|
I admit I had a hard time remembering what I was trying to do here. BTW, the notion of order of evaluation between labelled arguments is kind of meaningless, since it ends up being determined by the order of labels in the function definition rather than in the application (the same thing happens for records). |
|
A typical example of that is a partial application that only fixes a number of optional arguments, using LablTk for instance. The modified code it going to generate a partial application for each (non-consecutive) optional argument passed, rather than one for the whole block of functional arguments. The removal of the event was also intentional, I believe: the corresponding application corresponds to nothing in the source code. |
|
Would you mind giving a concrete example of code that would benefit from this optimisation? I can't quite picture what you mean. |
|
Imagine the following function: let many_opts ?opt1 ?opt2 ?opt3 ?opt4 ?opt5 ?opt6 ?opt7 ?opt8 ?opt9 ?opt10
?opt11 ?opt12 ?opt13 ?opt14 ?opt15 ?opt16 ?opt17 ?opt18 ?opt19 ?opt20 () =
[opt1;opt2;opt3;opt4;opt5;opt6;opt7;opt8;opt9;opt10;opt11;opt12;opt13;opt14;
opt15;opt16;opt17;opt18;opt19;opt20]
let some_opts = many_opts ~opt5:3 ~opt10:15 ~opt15:7 ~opt20:12With the current optimization, this partial application generates one abstraction block wrapping one application block. |
|
Thanks for the example. This patch does cause it to produce less efficient code. But we already produce such code for: let many_opts ~opt1 ~opt2 ~opt3 ~opt4 ~opt5 ~opt6 ~opt7 ~opt8 ~opt9 ~opt10
~opt11 ~opt12 ~opt13 ~opt14 ~opt15 ~opt16 ~opt17 ~opt18 ~opt19 ~opt20 () =
[opt1;opt2;opt3;opt4;opt5;opt6;opt7;opt8;opt9;opt10;opt11;opt12;opt13;opt14;
opt15;opt16;opt17;opt18;opt19;opt20]
let some_opts = many_opts ~opt5:3 ~opt10:15 ~opt15:7 ~opt20:12The optimisation currently applied for optional arguments:
So I propose to merge this bugfix for 4.14. If we really wanted to optimise these examples, the right way to do it would be to improve the code produced by the middle-ends in such cases. When the call is direct like this it is a valid optimisation to combine the results of these partial applications into a single application. |
|
@lpw25 You are comparing apples with oranges. Of course we need to produce inefficient code if the arguments are not optional, otherwise we would break the semantics. And you would have a hard time finding such a function. But for optional arguments, it was explicitly decided that eta-expansions were to be allowed, to avoid making the code generator explode in some cases. This is reasonable because having side-effects occur between optional arguments makes no sense. However, the way this optimisation is implemented is indeed buggy, because it delays not only the evaluation of the function (which is fine), but also the evaluation of the arguments (which is not). My concern is that I don't fully remember how different optimizations interact, so I would like at least to verify that this change doesn't have a serious impact on code size for labltk or lablgtk, which do contain a few such applications. (And I would first have to check where they are; maybe only in examples.) If the impact is small, then the simplification is welcome. Otherwise, it should be possible to fix the optimization rather than remove it. |
This makes no sense to me. The resulting semantics are just broken by any reasonable standard. For example: # let bar ?b ?c () =
print_endline "bar applied";;
val bar : ?b:'a -> ?c:'b -> unit -> unit = <fun>
# let baz p ?b ?c () =
print_endline ("bar applied to" ^ string_of_bool p);;
val baz : bool -> ?b:'a -> ?c:'b -> unit -> unit = <fun>
# let foo ?a =
print_endline "foo applied";
match a with
| None -> bar
| Some p -> baz p;;
val foo : ?a:bool -> ?b:'a -> ?c:'b -> unit -> unit = <fun>
# foo ~a:true;;
foo applied
- : ?b:'_weak4 -> ?c:'_weak5 -> unit -> unit = <fun>
-
# foo ~a:true ~c:5;;
- : ?b:'_weak3 -> unit -> unit = <fun>I cannot see how this isn't a bug caused by an incorrect optimisation. |
|
This is just a question of how you define the semantics. If you define from the beginning that side effects resulting from the application of optional arguments may be delayed until all the following optional arguments are either applied or discarded, then you can optimize and avoid code blow-up. The code blow-up was a serious problem, and led to choosing this semantics. Another optimization that we do is I believe that the impact of |
|
Would you mind trying to check how this change affects the code size of labltk or lablgtk? Or describing how someone else might go about it? |
|
Sorry for the delay. I will look into that, and check with lablgtk/labltk. |
173842c Merge flambda-backend changes ed7eba2 Remove leading space from LINE. (oxcaml/oxcaml#484) bd61170 Bump magic numbers (ocaml#5) c50c47d Add CI builds with local allocations enabled 1412792 Move local allocations support behind '-extension local' 6d8e42a Better tail call behaviour in caml_applyN c7dac3d Typemod: toplevel bindings escape even if no variables are bound 82d6c3e Several fixes for partial application and currying d05c70c Pprintast support for new local syntax e0e62fc Typecheck x |> f y as (f y x), not ((f y) x) d7e34ce Remove autogeneration of @ocaml.curry b9a0593 Port oxcaml/oxcaml#493 0a872d9 Code review fixes from oxcaml/oxcaml#491 6c168bb Remove local allocation counting 3c6e7f0 Code review fixes from oxcaml/oxcaml#478 bb97207 Rename Lambda.apply_position a7cb650 Quieten Makefile when runtime dep files are not present c656dc9 Merge flambda-backend changes 11b5424 Avoid printing double spaces in function argument lists 7751faa Restore locations to Typedtree.{pat,let}_bound_idents_full e450b6c add build_ocaml_compiler.sexp 0403bb3 Revert PR 9895 to continue installing VERSION b3447db Ensure new local attributes are namespaced properly 7f213fc Allow empty functions again 8f22ad8 Bugfix: ensure local domain state is initialised 80f54dd Bugfix for Selectgen with regions e8133a1 Fix external-external signature inclusion 9840051 Bootstrap d879f23 Merge remote-tracking branch 'jane/local-reviewed' into local-merge 94454f5 Use Local_store for the local allocations ref 54a164c Create fewer regions, according to typechecking (ocaml#59) 1c2479b Merge flambda-backend changes ce34678 Fix printing of modes in return types 91f2281 Hook mode variable solving into Btype.snapshot/backtrack 54e4b09 Move Alloc_mode and Value_mode to Btype ff4611e Merge flambda-backend changes ce62e45 Ensure allocations are initialised, even dead ones 6b6ec5a Fix the alloc.ml test on 32-bit builds 81e9879 Merge flambda-backend changes 40a7f89 Update repo URL for ocaml-jst, and rename script. 0454ee7 Add some new locally-allocating primitives (ocaml#57) 8acdda1 Reset the local stack pointer in exception handlers (ocaml#56) 8dafa98 Improve typing for (||) and (&&) (ocaml#55) 8c64754 Fix make_check_all_arches (ocaml#54) b50cd45 Allow arguments to primitives to be local even in tail position (ocaml#53) cad125d Fix modes from or-patterns (ocaml#50) 4efdb72 Fix tailcalls tests with inlining (ocaml#52) 4a795cb Flambda support (ocaml#49) 74722cb Add [@ocaml.principal] and [@ocaml.noprincipal] attributes, and use in oo.mli 6d7d3b8 Ensure that functions are evaluated after their arguments (flambda-backend ocaml#353) 89bda6b Keep Sys.opaque_identity in Cmm and Mach (port upstream PR 9412) a39126a Fix tailcalls within regions (ocaml#48) 4ac4cfd Fix stdlib manpages build 3a95f5e Merge flambda-backend changes efe80c9 Add jane/pull-flambda-patches script fca94c4 Register allocations for Omitted parameter closures (ocaml#47) 103b139 Remove various FIXMEs (ocaml#46) 62ba2c1 Bootstrap a0062ad Allow local allocations for various primitives (ocaml#43) 7a2165e Allow primitives to be poly-moded (ocaml#43) 2af3f55 Fix a flaky test by refactoring TypePairs (ocaml#10638) 58dd807 Bootstrap ee3be10 Fix modes in build_apply for partial applications fe73656 Tweak for evaluation order of labelled partial applications (ocaml#10653) 0527570 Fix caml_modify on local allocations (ocaml#40) e657e99 Relax modes for `as` patterns (ocaml#42) f815bf2 Add special mode handling for tuples in matches and let bindings (ocaml#38) 39f1211 Only take the upper bounds of modes associated with allocations (ocaml#37) aec6fde Interpret arrow types in "local positions" differently c4f3319 Bootstrap ff6fdad Add some missing regions 40d586d Bootstrap 66d8110 Switch to a system with 3 modes for values f2c5a85 Bugfix for Comballoc with local allocations. (ocaml#41) 83bcd09 Fix bug with root scanning during compaction (ocaml#39) 1b5ec83 Track modes in Lambda.lfunction and onwards (ocaml#33) f1e2e97 Port ocaml#10728 56703cd Port ocaml#10081 eb66785 Support local allocations in i386 and fix amd64 bug (ocaml#31) c936b19 Disallow local recursive non-functions (ocaml#30) c7a193a GC support for local allocations (ocaml#29) 8dd7270 Nonlocal fields (ocaml#28) e19a2f0 Bootstrap 694b9ac Add syntax to the parser for local allocations (ocaml#26) f183008 Lower initial stack size 918226f Allow local closure allocations (ocaml#27) 2552e7d Introduce mode variables (ocaml#25) bc41c99 Minor fixes for local allocations (ocaml#24) a2a4e60 Runtime and compiler support for more local allocations (ocaml#23) d030554 Typechecking for local allocations (ocaml#21) 9ee2332 Bugfix missing from ocaml#20 02c4cef Retain block-structured local regions until Mach. 86dbe1c amd64: Move stack realloc calls out-of-line 324d218 More typing modes and locking of environments a4080b8 Initial version of local allocation (unsafe) git-subtree-dir: ocaml git-subtree-split: 173842c
|
Any more thoughts here @garrigue? |
|
I rebased on 5.1.0 and checked with lablgtk2. If the size increase in |
|
My non-expert impression is that the current behavior is surprising, there is a consensus to consider it buggy, and it comes with extra implementation complexity. A 5% code size increase in very rare programs sounds like a fine price to pay to simplify our implementation and fix a bug. |
|
I have proposed an alternative fix in #12720. |
It seems that I missed this comment. let f ?a = print_endline "After a";
fun ?b -> print_endline "After b";
fun ?c -> print_endline "After c";
fun ~d -> print_endline "After d";
fun e -> print_endline "After e";;
val f : ?a:'a -> ?b:'b -> ?c:'c -> d:'d -> 'e -> unit = <fun>
let g = f ~c:3;;
val g : ?a:'a -> ?b:'b -> d:'c -> 'd -> unit = <fun>
g ~a:3;;
- : ?b:'_weak8 -> d:'_weak9 -> '_weak10 -> unit = <fun>
g ~a:3 ~d:3;;
- : ?b:'_weak11 -> '_weak12 -> unit = <fun>
g ~a:3 ~b:3;;
After a
After b
After c
- : d:'_weak13 -> '_weak14 -> unit = <fun>Note that, if we assume that any block of optional parameters is followed by at least a non-optional one (which is required to be able to force them to default), one should not be able to observe the difference. Also, while 5% may seem small, the concern is that, without this optimization, a single partial application can generate dozens of closures for basically useless enforcement of evaluation order. I would not expect this to happen in absence of optional arguments, because one does not write a function with dozens of parameters when they need to be passed for every call. |
I agree with that. What I don't agree on is to miscompile code that doesn't follow this convention. And moving arbitrary code under a lambda is definitely a miscompilation in a language with side effects. So in all cases, I think we should merge #12720. But I think we should also fix the miscompilation, and I can propose several alternatives:
I will work on the second solution, so that we can look at a concrete proposal. I will also rebase this PR on top of #12720 when I have some time. |
My impression was that such functions are useless: the last parameter cannot be omitted at application sites, so it could just as well have been declared as non-optional. Isn't that the case? At the very least such functions already produce a stern warning 16: |
|
It depends on what you call useless. I certainly can't find any good reason to write something like that explicitly. |
|
But from a user's perspective, it could be used to more easily pass options: let f ?(x = 0) = ...
let g () = f ~x:1 (* instead of ~x:(Some 1) *)
let h () = f ?x:NoneFinally, in a few corner cases you could have actual erasable optional parameters in last position: let f ?(x = 0) = let y = x + 1 in fun () -> y (* No warning *) |
There is something I don't understand here. Prohibiting completely that kind of code would be a problem for lablgtk, which does things like: let make_button_params ~cont pl ?label ?use_stock ?use_underline ?relief =
let pl = (
may_cons P.label label (
may_cons P.use_stock use_stock (
may_cons P.use_underline use_underline (
may_cons P.relief relief pl)))) in
cont pl
let pack_return create p ?packing ?show () =
pack_return (create p) ~packing ~show
let button ?label =
Button.make_params [] ?label ~cont:(
pack_return (fun p -> new button (Button.create p)))I.e. it combines sets of optional parameters using CPS. Also, I'm not too enthusiast about depending on optimizations done in the middle end, as they may just fail. For instance if one goes through a functor. |
|
Here is an example where pushing optional arguments down makes no sense: # let mk y =
let f ?(init = 0) =
let r = ref init in
y r
in
f;;
val mk : (int ref -> 'a) -> ?init:int -> 'a = <fun>
# let foo r =
fun ~a ~b () ->
r := a * !r + b; !r;;
val foo : int ref -> a:int -> b:int -> unit -> int = <fun>
# let add = (mk foo) ~init:0 ~a:1;;
val add : b:int -> unit -> int = <fun>
# let mul = (mk foo) ~init:1 ~b:0;;
val mul : a:int -> unit -> int = <fun>
# let () =
print_int (add ~b:3 ()); print_newline ();
print_int (add ~b:5 ()); print_newline ();
print_int (mul ~a:3 ()); print_newline ();
print_int (mul ~a:5 ()); print_newline ();
();;
3
8
3
5Here,
I've read the manual section about labelled applications, and it doesn't contain any mention of unspecified behaviours around optional arguments (unlike for evaluation order, which is explicitly unspecified).
So if we follow the manual strictly (with my interpretation) we must delay evaluation of the function to its first arguments if at least one argument is missing. I don't like that, but at least it's better than randomly changing evaluation order depending on the order of the labels. |
|
@lthls I agree that the manual is not precise enough concerning when then body of a function will be evaluated after an out-of-order partial application. The statement "the result of the function will still be a function of these missing parameters to the body of f" was intended in a mathematical way, not as meaning that we build a function that takes all missing arguments. My preferred solution is to update the manual indicating that side-effects after a non-optional argument are guaranteed to happen as soon as possible, but that those after optional arguments may be delayed. And possibly make the warning stricter. On the other hand, if there is a consensus that unspecified behaviors are bad, then implementing your interpretation of the current text, i.e. that all side-effects are delayed until all arguments preceding the last one that was passed in the partial application are delayed, seems OK too. This is trivial to implement, and only beneficial to code size. This may change the behavior of some (rare) existing programs, but they should have read the manual :-) |
|
Even if we go for the "maximal delay" approach, it might still be good to update the manual to indicate explicitly that both the arguments and the function itself are evaluated before building the closure, which itself is only abstracted on arguments preceding the last given one. |
|
I've actually found a bug: |
|
I've run some tests to see the impact of various versions on the code size of lablgtk (building the last release with the default build instructions). In addition to trunk and this PR, I've added a branch where I add an optimisation (in Simplif) to recover partial applications when it is safe to do so. Here are my results (sizes in bytes):
The optimisation doesn't work in Closure because the scheme for compiling toplevel values transforms direct calls ( My conclusion: if we go for the semantics of this PR, we do increase code size a little bit, and not all of that can be recovered with semantically correct optimisations. But I'm still in favour of merging it (without the extra optimisation). |
8ed23d4 to
a1a55d4
Compare
a1a55d4 to
c0a8271
Compare
|
The CI produced this error: Has anyone seen this error before ? |
Yes - on a PR this morning which only updated GitHub metadata… https://github.com/ocaml/ocaml/actions/runs/6823859603/job/18558924939 |
|
@lthls Thank you for your testing. Note that the let many ?(arg1=print_endline"arg1") ?(arg2=print_endline"arg2")
?(arg3=print_endline"arg3") ?(arg4=print_endline"arg4")
?(arg5=print_endline"arg5") ?(arg6=print_endline"arg6")
?(arg7=print_endline"arg7") ?(arg8=print_endline"arg8")
?(arg9=print_endline"arg9") ?(arg10=print_endline"arg10")
?(arg11=print_endline"arg11") ?(arg12=print_endline"arg12")
?(arg13=print_endline"arg13") ?(arg14=print_endline"arg14")
?(arg15=print_endline"arg15") ?(arg16=print_endline"arg16")
?(arg17=print_endline"arg17") ?(arg18=print_endline"arg18")
?(arg19=print_endline"arg19") ?(arg20=print_endline"arg20") =
print_endline "all options"; fun () -> print_endline "all args"
let f = many ~arg10:() ~arg20:()
let g = f ~arg19:()
let () = g ()This function takes 20 optional arguments (which, as seen in lablgtk, is not shocking), and applies them partially twice before doing a full application. The code size (on macos-arm64) is
This is not really surprising: by forcing evaluation after each argument we end up really creating dozens of closures. I can see at least 3 possible choices
All have advantages and drawbacks. An extra factor that may be considered is the existence of an untyped semantics. I'll try to come up with a good summary for the meeting this week. |
|
Note that I originally thought that the current approach guaranteed that side-effects following a non-optional argument would never be delayed, but this is not the case. The real property is that non-optional arguments that are not immediately between optional arguments (without unlabeled arguments) will never be delayed. A way to guarantee that is to require that any n-ary abstraction that contains optional arguments must contain an unlabeled argument after them. Unfortunately, this does not allow combining blocks of optional arguments in the way of lablgtk. |
|
Since we could not talk about this at the developer meeting, let me re-summarize the issues and potential solution. In the following I will say parameters for the arguments of a function, either seen in its type or definition, and arguments for the arguments in a function application. The question here is how to compile partial applications of functions that have labelled and optional parameters, and how this impacts side-effects. There are several aspects to consider
One can think of several specifications; I give a reduction semantics later.
Strict-eager is implemented by this PR. The advantage is that it is easy to give it a deterministic semantics. The drawback is that it can be very costly for out-of-order partial applications, because it requires creating two closures for each omitted argument: one to receive it, the other to apply the original function. Unlabeled-strict is implemented by trunk. The implementation itself is deterministic, but it is difficult to give a deterministic reduction semantics for it, as compilation follows the types. We show a non-deterministic semantics that seems sufficient to reason about programs. The advantage is that, in case of out-of-order partial application, large numbers of contiguous optional arguments can be abstracted and applied simultaneously, resulting in much shorter generated code, and easier to optimize. Note also that, if we require than any optional parameter be followed by an unlabeled parameter inside the same n-ary abstraction, the semantics coincides with strict-eager. However, lablgtk for instance would not fulfill this requirement. Maximal delay is simple to define and compile, and probably the most efficient in terms of code size. I also think that it is reasonably easy to understand for users. However, it seems hard to define an untyped semantics, as the compilation depends inherently on knowing statically the order of all the parameters of the function. This can be seen in its type, but it may require evaluation to become visible in values. Also, compared to trunk, it changes the semantics by delaying some side-effects, which may be observable. Last I give a reduction semantics, that applies for both strict-eager and unlabeled-strict (nondeterministically in that case). (* labels *)
l ::= * | lab | ?lab (* * stands for no label *)
(* strict-eager values *)
v ::= fun l:x -> e
| (fun l:x -> e) {l_i:v_i}_i∈I l ≠ l_i (i∈I), l not optional
| (fun ?l:x -> e) {l_i:v_i}_i∈I l ≠ l_i, ?l ≠ l_i, * ≠ l_i (i∈I)
(* unlabeled-strict values; any application of an optional parameter
to only labelled arguments is a value *)
v ::= fun l:x -> e
| (fun l:x -> e) {l_i:v_i}_i∈I l ≠ l_i (i∈I), l not optional
| (fun ?l:x -> e) {l_i:v_i}_i∈I * ≠ l_i (i∈I)
(* I denotes a set of ordered indices. The indices themselves are irrelevant. *)
{l_i:v_i}_i∈I = {l_η(i):v_η(i)}_η(I) (η strictly monotone)
(* expressions *)
e ::= v
| e {l_i:e_i}_i∈I
(* The input program is preprocessed so that in n-ary applications,
unlabelled arguments have the highest indices.
This invariant is not kept by reduction (in particular merging) *)
(fun l_j:x -> e) {l_i:v_i}_i∈I --> ([v_j/x]e) {l_i:v_i}_i≠j l_k≠l_j (k<j), l_j not optional
(fun l_j:x -> e) {l_i:v_i}_i∈I --> ([v_j/x]e) {l_i:v_i}_i≠j l_k≠l_j, l_k≠* (k<j), l_j optional
(fun ?l_j:x -> e) {l_i:v_i}_i∈I --> ([Some v_j/x]e) {l_i:v_i}_i≠j l_k≠l_j, l_k≠?l_j, l_k≠* (k<j)
(fun ?l:x -> e) {l_i:v_i}_i∈I --> ([None/x]e) {l_i:v_i}_i∈I ∃j∈I, l_j=*, l_k≠l, l_k≠?l (k<j)
v {l_i:v_i}_i∈I {l_j:v_j}_j∈J --> v {l_i:v_i}_i∈I∪J I < J |
|
Thanks @garrigue for the detailed explanation ! I'll add my own thoughts where I think I have something useful to add.
I believe trunk implements a slightly stricter variation of that, where
It wasn't completely clear to me what the two closures where; if I understand correctly, one is the closure created naturally by partial application of the initial function, and the other is the one inserted explicitly by the code in
In addition, |
It took me a while to recall it (I thought about all that when writing the original |
I was thinking about the following situation. Suppose that we have a function and apply it partially. let f ?arg1 ?arg2 ?arg3 () = ...
let g = f ~arg3:3Then, in the strict eager case, the code for let g = fun ~arg1 -> let g1 = f ~arg1 in fun ~arg2 -> g1 ~arg2 ~arg3:3So for each omitted argument we need an abstraction and an application. |
Ah, I see. Somehow I thought that giving any non-optional parameter would erase any previous omitted optional parameters, but I see that it is not the case. Thanks for the clarification. |
I stand by my position earlier in the discussion: I consider the current behaviour a bug (if you prefer it can be considered a bug in the specification rather than the implementation). The fix is correct and seems to only negatively affect uncommon corner cases. With a reasonable amount of effort the middle-ends could be made to understand partial application directly (rather than eta-expand it out), which I believe would be sufficient to optimise this out in the places where it was known to be safe. Although whether that is worth doing should be assessed independently. I would also be in favour of a change to emit an error for functions that end with an optional parameter, which would then allow the existing code to be kept. That's obviously not backwards compatible, but it's pretty close to the existing warning so probably doesn't break much. It would be good for someone to check how that looks on opam. |
|
@lpw25 Could you be clearer on what you mean by "bug in the specification" ? For the stricter warning, it is implemented in #12739. It is somehow more intrusive than I expected, as it required a number of changes in ocamlc itself (i.e. it prevents doing eta-expansion with only an optional argument). Checking on opam would indeed be required. |
|
I mean that you have provided a non-deterministic semantics that carefully and precisely describes the set of bad behaviours currently implemented. Letting users write |
|
I'm not sure I agree with your assessment. The next step is probably to test #12739 on opam. If the impact is small enough, merging it solves the problem by making the difference invisible. But if it's not, we will have again to discuss that. |
Fixes #10652.
One part of this patch removes some code that changes the behaviour when all previous arguments were optional. I'm not sure what it was supposed to help with, and none of the tests in the testsuite rely on it.
The second part switches from
List.fold_lefttoList.fold_rightfor emitting bindings, as it corresponds to a right-to-left evaluation order for arguments, which is more coherent with normal applications.I think the result is more coherent (and it fits @lpw25's expectations for his test case), but I would welcome discussion on the purpose of the original code.