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
A new check that 'let rec' bindings are well formed #556
Conversation
@stedolan has given an additional example which is accepted by both the existing check and the check in this pull request: # let rec r = (let rec x = `A r and y = fun () -> x in y);;
val r : unit -> [> `A of 'a ] as 'a = <fun>
# let (`A x) = r () in x ();;
Segmentation fault At the moment it's not clear to me whether the problem is that the check is too permissive or that the translation of nested recursive bindings is incorrect. |
I believe that this constraint is sufficient to restrict programs to those which could be compiled. However, I'm not sure it is strong enough to restrict programs to those which OCaml's current backends can compile (even after fixing the issue with nested recursive bindings). The problem is that OCaml currently handles recursive bindings by allocating a dummy block before executing the bindings and then filling in the fields of the block from the defined value. In order to do this is must know the size of the result in advance. For example, the following code is accepted by the check in this patch but refused by the old check, and results in a segfault at runtime: let foo p x =
let rec f =
if p then (fun y -> x + g y) else (fun y -> g y)
and g =
if not p then (fun y -> x - f y) else (fun y -> f y)
in
(f, g)
let f, g = foo true 9
let _ = f 10
let _ = g 11 As I said, I believe that any code which passes the constraint in this patch can be compiled. For example, the above definition could be translated to something equivalent to: let f, g =
match p, not p with
| true, true ->
let rec f = (fun y -> x + g y)
and g = (fun y -> x - f y) in
f, g
| true, false ->
let rec f = (fun y -> x + g y)
and g = (fun y -> f y) in
f, g
| false, true ->
let rec f = (fun y -> g y)
and g = (fun y -> x - f y) in
f, g
| false, false ->
let rec f = (fun y -> g y)
and g = (fun y -> f y) in
f, g but until the OCaml backend is smart enough to do this I think the check will need to be stronger. |
Thanks, @lpw25 . One approach that would both fix the open PRs and avoid introducing new unsoundness would be to keep both checks (i.e. on Lambda and Typedtree) for the moment. |
Yes, I think that is a good idea. Fixes the immediate issues with substantially lower risk. |
Here's another size-related problem with the new check: # let rec y = (if true then [| |] else y); [| |] in y;;
Characters 26-31:
let rec y = (if true then [| |] else y); [| |] in y;;
^^^^^
Warning 10: this expression should have type unit.
Fatal error: exception File "bytecomp/bytegen.ml", line 163, characters 47-53: Assertion failed |
typing/typeclass.ml
Outdated
let ids, exprs = | ||
List.split | ||
(List.map | ||
(fun (id, _, _, _, _, _, _, _, _, _, _, {ci_expr}) -> id, ci_expr) |
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 should not have to write this. What about some recordification work in typeclass.ml
?
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.
That's a good idea, and I think it'd make a suitable project for a compiler hacking evening. I've added it to the wiki.
Excellent, exemplare PR description! (Your example of issue not caught by Merlin is Have you thought of expressing the analysis as a usage-aware typing system? Not only could that be an excellent proposal for the ML workshop, and provide a readable, declarative presentation of the analysis, I think that it could also clarify some subtle point, such as the way you propagate information from the left-hand expression of a I'm worried about the In the future, we can ask about giving some usage-type information in value definitions in interfaces, and this would let us make the check more modular -- un-special-case |
|
||
let rec x = assert y and y = true;; | ||
|
||
let rec x = object method m = x end;; |
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.
It is not clear to me why this case is rejected (or the one below), given that method calls delay evaluation (object method m = fun () -> x end
is delayed as well). Isn't this just a matter of how object declarations are elaborated in practice, that could change with a refined implementation? If so, then maybe it would make sense to add a comment to say it explicitly.
I noticed with interest that you have a good testsuite; it may be helpful to add some comments on the less-obvious cases of the testsuite. Also, I think that your work would bring us much closer to the situation of having sensibly better error messages in this case. Instead of " this kind of expression is not allowed on the right-hand-side of a Finally, I'm lukewarm about the word "inspection". Wouldn't "evaluation" or "forcing" be clearer? (For example I find it clearer to say that |
Also, you forgot a Changes entry. |
This paper by Hirschowitz, Leroy and Wells is definitely relevant, and deals with the issue of correctly sizing blocks. It also contains a correctness proof, although I haven't read it enough to know how this differs from the current implementation. |
Is there a good reason why the rules for |
|
Indeed. I'm testing the patch against OPAM, and so far (about 15% into the list of packages) it's failing three packages:
let rec deep_cycle = `Tuple [| `Shared deep_cycle |]
let rec waiter = ref (Some handle_result)
and handle_result state =
...
let rec dummy_arc =
{ vert = Infinity; next = dummy_arc;
inst = ref (Terminal dummy_arc); mate = -1 } |
Given how late we are in the 4.03 release cycle, I think we should consider either delaying for 4.04 (remember, we are shooting for a shorter release cycle next time, so it's not as big of a deal), or making the stricter check optional, à la It seems rather clear to me, given the state of PR#7231, that more work will be needed both to refine your proposed check and the compiler backend. Given that refinement to your check will decrease its potential for compatibility-breaking changes, I think it would be a mistake to rush the current check by default on 4.03 users. |
I'm strongly leaning toward postponing. @garrigue what do you think? |
The question is how much we value soundness. If we keep the check in the backend, another option would be to make the typedtree check a warning. |
Note that the amount of breakage expected should be zero: it seems extremely unlikely to me that code in the wild would actually use an unsound recursive value definition. The examples that breaks type soundness using a recursively-defined GADT (PR#7215) could exist in theory, but it seems rather implausible. The examples that segfault by allowing unsound definitions (PR#7231) are unlikely to be present in released code, but they could be exploited by malicious users as security flaws. We already have evidence that the I think that making the added check optional could strike a nice balance: safety-conscious user could use the flag (I think that 4.03 comes with a feature to easily add a flag to all compilations; is this in Changes?), but we would have more time to study the check before imposing it on all codebases. |
This example from lwt:
relies on This is dubious: whether code is accepted by the compiler depends on the vagaries of the inliner (which are increasingly vague, thanks to Flambda). This also breaks the invariant that compiling with If this example is to be allowed, perhaps |
To be fair this relies on That being said I think it would be better not to special case |
I agree, and indeed it should be refused, according to the existing documentation. But we have to strike a balance between theory and practice: I'm really wary of introducing an incompatibility a few hours before the release, when we know some important packages will be broken, and we don't know of any problem caused by the bug in the wild. BTW, another broken package (due to |
I'm favour of releasing without the fix and then releasing the fix in 4.03.1 (which will probably be needed no matter how many bugs we fix before the release). That should give us a bit more scope to do things like change the |
That sounds like the prudent approach to me, too. |
I've added some new commits which address some of the comments above:
|
I didn't realise
Looking at the patch, I see that it treats |
Let's go with Leo's plan: release without this patch, then we'll have at least a few weeks to refine it and check compatibility. |
@gasche I'm in favor of merging the rebased PR. |
I also like it, so I went ahead and merged. Thanks @yallop. |
Over at MPR#7653 I started investigating what appeared to be, at first, an infinite loop of the compiler on a file of the Frama-C codebase, which I bisected and was provoked by the present PR. After trying to look at traces in various way, I am starting to realize that this may be not an infinite loop, but a sign of a performance issue related to the design of the current check: on certain large expressions, it may be that the environment grows to extremely large sizes. On the faulty file in the PR, I added logging to measure the size of the I have the impression that, if the environment structure is already large, then a sequence of let-bindings may result in each new variable being added with a fairly large use-set, resulting in a sort of exponential growth of the use-set size. I don't have a firm understanding of how precisely that would be happening yet. |
Ready the discussion again, this performance issue was already noticed by @yallop in #556 (comment) and he pushed a commit ( cbc66ff ) to fix it, but it seems that this commit was missed from later merges. I'm going to stop looking at this for the week-end, and hopefully get things as they should be next week, possibly with @yallop's help. |
I got in touch with Jeremy, and I will be taking care of merging cbc66ff back into trunk and 4.06, and checking that the performance regression went away. |
For fun, I ran the same size-instrumentation code on the patched version. Before: largest use set has 1 046 688 bindings, total sum 3 164 206 bindings After: largest use set has 2 bindings, total sum 82 bindings |
Just in case if you're interested, I've referenced a few examples of real-world code that was rejected by the new let-rec checker. I've read through the comments in this PR and didn't find anything that will state that such code should be rejected. And although, I'm pretty fine with the new behavior of the let-rec checker, I decided to submit an issue to Mantis, as I'm not sure whether this regression was anticipated or not. |
See #1712 |
* ARM64: improve recognition of immediate operands for logical instructions Now we recognize the full set of hardware supported immediate operands. `is_logical_immediate` was moved from Selection to Arch and made to work on nativeints. This is in preparation for future use in Emit. * ARM64: Improve code generation for load integer immediate Add a special case for immediates that can be loaded in one "or" instruction. Refactor the code that produces sequences of movz/movn/movk to be less redundant and to choose the shortest sequence between the "movz" sequence and the "movn" sequence. * ARM64: add and recognize specific operation for sign extension * Fix compilation Co-authored-by: Xavier Leroy <xavier.leroy@college-de-france.fr>
fe8a98b flambda-backend: Save Mach as Cfg after Selection (ocaml#624) 2b205d8 flambda-backend: Clean up algorithms (ocaml#611) 524f0b4 flambda-backend: Initial refactoring of To_cmm (ocaml#619) 0bf75de flambda-backend: Refactor and correct the "is pure" and "can raise" (port upstream PR#10354 and PR#10387) (ocaml#555) d234bfd flambda-backend: Cpp mangling is now a configuration option (ocaml#614) 20fc614 flambda-backend: Check that stack frames are not too large (ocaml#10085) (ocaml#561) 5fc2e95 flambda-backend: Allow CSE of immutable loads across stores (port upstream PR#9562) (ocaml#562) 2a650de flambda-backend: Backport commit fc95347 from trunk (ocaml#584) 31651b8 flambda-backend: Improved ARM64 code generation (port upstream PR#9937) (ocaml#556) f0b6d68 flambda-backend: Simplify processing and remove dead code (error paths) in asmlink (port upstream PR#9943) (ocaml#557) 90c6746 flambda-backend: Improve code-generation for inlined comparisons (port upstream PR#10228) (ocaml#563) git-subtree-dir: ocaml git-subtree-split: fe8a98b
…7) (ocaml#556) * ARM64: improve recognition of immediate operands for logical instructions Now we recognize the full set of hardware supported immediate operands. `is_logical_immediate` was moved from Selection to Arch and made to work on nativeints. This is in preparation for future use in Emit. * ARM64: Improve code generation for load integer immediate Add a special case for immediates that can be loaded in one "or" instruction. Refactor the code that produces sequences of movz/movn/movk to be less redundant and to choose the shortest sequence between the "movz" sequence and the "movn" sequence. * ARM64: add and recognize specific operation for sign extension * Fix compilation Co-authored-by: Xavier Leroy <xavier.leroy@college-de-france.fr>
This pull request introduces a new check that
let rec
bindings are well-formed, replacing the existing check in the compiler.Background: checking
let rec
In addition to types, there are two constraints on the form of a
let rec
binding:However, it is not necessary for the right-hand side of a
let rec
binding to be a function, or even to be in the form of a value. For example, the following is allowed:This reflects a more liberal policy (described in more detail below) than some other ML-family languages, such as SML98:
Code which makes use of OCaml's relaxed restrictions is fairly common (e.g. [1], [2])
Where should the check be performed?
An OCaml program takes on several forms as it passes through the compiler. Source code is scanned, parsed, type-checked, and translated into a series of intermediate forms, before finally emerging as byte code or machine code. At present the check for whether a
let rec
binding is well-formed is performed on one of these intermediate languages, called Lambda.Advantages of Lambda
Lambda is a good place to perform the check, for several reasons.
First, it's a comparatively small, simple language. Source expressions that have quite different syntax and types may take the same form when converted to Lambda. For example, the following expression, that builds a record
is translated to the same Lambda code as the following expression, which constructs a value of variant type:
Since the Lambda language is smaller than the languages used earlier , the check is also smaller and simpler.
Second, the Lambda language is comparatively stable. It is often possible to leave Lambda unchanged when adding features to the OCaml language. As long as Lambda remains unchanged, the check that
let rec
bindings --- or rather, their translation into Lambda code --- can also be left unchanged.Third, the form of Lambda code reflects the run-time behaviour of OCaml programs quite closely. Whether a
let rec
binding is considered well-formed is in a sense determined by the behaviour of the program, not by its type or surface syntax. It's easier to determine how a program will behave at the point when it's in Lambda form than at earlier stages of the compiler.Advantages of Typedtree
However, there are also a number of advantages to placing the check for
let rec
wellformedness at an earlier stage in the compiler.First, developing an OCaml program often involves running only a part of the compiler. For example, Merlin uses the OCaml type checker to check the types of a program as you type it into your editor. However, Merlin does not perform the additional work of translation into Lambda code at this stage, and so the check that
let rec
bindings are well-formed does not take place. It sometimes happens that Merlin reports no errors in a program such as this one:which is then rejected when it is eventually passed to the OCaml compiler. If the
let rec
check took place during type checking then Merlin could offer more comprehensive feedback to the user at an early stage, and every program accepted by Merlin would be guaranteed to compile successfully.Similarly, the multi-stage language MetaOCaml uses OCaml's parser and type checker to ensure that code that appears within quotations is correct. However, checking quoted code does not involve translation into Lambda, and so the
let rec
check is not performed. Instead, MetaOCaml performs a much simpler and more restrictive check which rejects some valid OCaml programs:Third, performing the check on Lambda makes its behaviour more difficult to explain to users. Lambda is an internal language of the compiler, not part of the OCaml definition. However, the
let rec
check is defined by a combination of the translation into Lambda and a test of the resulting Lambda code. It is difficult to explain precisely which programs will be accepted or rejected without also describing the translation.Finally, some recent additions to the langauge make it more difficult to check
let rec
bindings using Lambda code. A few days ago @stedolan posted a program on Mantis that uses a combination of GADTs and alet rec
binding to cause OCaml to crash. Unfortunately, there is not enough information available in the Lambda form of his program to distinguish it from harmless programs that should be accepted. Moving the check to an earlier phase of the compiler would make it possible to distinguish the harmful program from the harmless program before the information is thrown away.How does the new check work?
This pull request adds a new implementation of the
let rec
check to the type checking phase of the OCaml compiler. At this stage, programs are represented in the Typedtree language. Programs in the the Typedtree form still resemble source programs quite closely, but they have been checked for type correctness, and annotated with some additional useful information, such as a way of distinguishing identifiers with the same name.The new check distinguishes various ways of accessing a variable.
f
, match against a variantv
, or access a field of a recordr
.x
is an unguarded use of the variablex
.[x]
,{ l = x }
andfun () -> x
are all guarded uses of the variablex
(x; ())
The way that a variable is used in an expression is determined both by the form of the expression and the way that the variable is used in subexpressions. Here are some examples:
Some
changes uses of a variable in its argument from unguarded to guarded, but does not affect inspections. For example, inSome f
,f
is guarded, but inSome (f x)
,f
is used.fun () -> f x
, bothf
andx
appear guarded.The full
let rec
check involves tracing the uses of variables through bindings. For example in the following expression,y
is considered guarded, because it appears guarded in the binding forx
, and because the bound variablex
is not inspected:However, in the following expression,
y
is considered to be inspected, because its value may be inspected by the functionf
:Tracing uses in this way necessarily involves some approximation, and in some cases the implementation in this PR approximates quite coarsely. For example, any appearance of a variable within a class or recursive module definition is treated as an inspection.
These different types of variable access are key to the definition of
let rec
wellformedness, which is as follows: every access to a variable bound bylet rec
in its own definition must be guarded.Examples and changes
Bug fixes
Moving the check from Lambda to Typedtree fixes several open bugs, including
Mantis PR7215: Unsoundness with GADTs and let rec
Here is the offending code, which caused a segmentation fault:
This is now rejected, because
p
is inspected in its own definition.Mantis PR7231: let-rec wellformedness check too permissive with nested recursive bindings
Here is the offending code, which caused a segmentation fault:
This is now rejected because the call to
y
involves inspectingx
, and inspectingx
involves inspectingr
, which is not allowed.Mantis PR6738: Check for the well-formedness of let rec earlier, before or at type checking
There should no regression to the
let rec
problems that have been previously reported and fixed, such asIncompatibilities
Moving the
let rec
check from Lambda to Typedtree also introduces a number of small incompatibilities:Array constructors are now considered inspections.
Should the following program be accepted or rejected?
In the current implementation the answer depends on the type of
z
. Ifz
has some concrete type, such asint
orfloat
, then the program is accepted. In other cases, such as wherez
is bound as the parameter of a polymorphic function, the program is rejected.The new check is simpler, but a little less generous, and rejects the program regardless of the type of
z
.A call to
ref
is now considered an inspection.The following program is accepted by the current implemenation, even though
y
is passed to the functionref
:The new check rejects the program, since
ref
is not distinguished from other functions at the type-checking stage. However, the program can be easily rewritten to an acceptable form:Record overriding is always considered an inspection, even when there are no fields retained:
The following rather odd program is currently accepted, but rejected by the new check:
Other improvements
A few programs that were previously rejected are now accepted. For example, this program was previously accepted
while this one was rejected
With the new check both are accepted.
I'm grateful to @mshinwell, @lpw25, @garrigue and @stedolan for help, suggestions and testing.
I would very much appreciate a review of both the new approach and the new code.