-
Notifications
You must be signed in to change notification settings - Fork 77
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
Overwriting primitives in lambda #3107
Conversation
mode : alloc_mode; (* Alloc mode of resulting block *) | ||
} | ||
|
||
and reset_field = |
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.
As @anfelor and I discussed over Slack, I think it would be a significant win to change the representation here, including reset_field
elements only when we are actually changing a field. (One way to understand this is the observation that 'a option list
is isomorphic to (int * 'a) list
, ignoring trailing None
s.) Not sure what the consumer of this is going to look like, so maybe we can do something even better than an int
to count the missing fields, but storing an int
is better than making a list with likely a lot of None
s in it.
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.
See bytegen.ml
for a consumer of this representation
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.
Note that each None
corresponds to an underscore in the source program (eg. overwrite x with (_,3)
). We should thus expect the number of None
s to be somewhat small and should focus just on what the best representation is for code quality.
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 would have thought the list representation is easier actually.
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.
Which list representation do you mean? The 'a option list
in the PR or the (int * 'a) list
proposed by Richard?
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.
And I don't think the number of None
s will be all that small, in the case of record updates: {x with f}
can have an arbitrarily large number of None
s.
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 Mark seemed happy with the current design, I would propose we keep the representation as it is now.
@@ -1713,6 +1732,10 @@ let primitive_may_allocate : primitive -> alloc_mode option = function | |||
| Pmakefloatblock (_, m) -> Some m | |||
| Pmakeufloatblock (_, m) -> Some m | |||
| Pmakemixedblock (_, _, _, m) -> Some m | |||
| Preuseblock { mode } -> Some mode |
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.
But these new primitives can't allocate, can they? It's one of the reasons I don't think the mode
is necessary.
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 mode
will soon also contain uniqueness information that informs us whether the new block will itself be used uniquely. One worry then might be if the optimizer can lift out Preuseblock
s that are unique? (In fact, it can't: it could only do that if the cell we reuse is statically known, but that cell will have been marked as unique and can not be lifted out). Furthermore, we sometimes handle Pmakeblock
and Preuseblock
similarly (eg. in tmc.ml
) and it might be helpful if they have the same fields.
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.
This line is in the function primitive_may_allocate
, and returning Some mode
makes it sound like it might allocate. (Perhaps my comment about the mode
buried my main point.)
One worry: it does allocate in bytecode. Does that matter here?
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 would be sound to keep the Some mode
here, since primitive_may_allocate
is only used in transl_primitive
where this value causes strictly more checks. Since we have decided to map Preuseblock
to Pmakeblock
in the backend for the moment, I think we should stick with this code for now. However, I will add a CR to make all of these return None
once the backend supports actual overwriting. The bytecode interpreter does not use stack allocation so this check is irrelevant there.
let choices = List.map (choice ctx ~tail:false) blockargs in | ||
match Choice.find_nonambiguous_tmc_call choices with | ||
| Choice.No_tmc_call args -> | ||
Choice.lambda @@ Lprim (Pmakeblock (tag, flag, shape, mode), args, loc) | ||
let block = Constr.makeblock (tag, flag, shape, is_reusing, mode) in | ||
Choice.lambda @@ Lprim (block, args, loc) |
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's not clear to me that args
should be the same here for the two cases of is_reusing
. That is, when is_reusing
says to allocate, args
must be all the fields of the constructor. But when is_reusing
says to reuse, args
is only the changed fields. But that's not what this code does.
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.
Yes, for the tmc
analysis we only need to consider the changed fields, since only they can contain a recursive call. The same code works in both cases.
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 would expect some tests for the treatment here -- even if the tests just print the lambda code and then fall over. Or at least a loud comment somewhere reminding us to test this thoroughly before release. A mistake here could lead to miscompilation.
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.
This, too, should get a bunch of testing.
I agree that the additions to |
OK. I'll focus there and then we can return to this one. |
(* In-place reuse of heap blocks. | ||
Each of these takes the allocation to be reused as first argument. | ||
The following arguments correspond to the fields that are Reuse_set. *) | ||
| Preuseblock of |
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.
@chambart and I were just discussing that it's probably easier if these primitives return the block which has been updated, so that subsequent uses of the unique block use the new variable (bound to the result of such primitives). This may help produce a more straightforward handling of the primitives in Flambda 2, in particular to help enforce the necessary "barrier" behaviour.
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.
Oh, right, that is the intended meaning. If Preuseblock
was a function, we should have: Preuseblock : memory cell -> overwritten args -> new block
. You should not refer to the overwritten memory cell after it was passed to Preuseblock
. In fact, referring to the overwritten memory cell again would break lots of properties since the overwrite may change its type.
I have rebase the changes in this PR onto main and created a draft PR at #3350 |
This adds the basic primitives needed to support overwriting to lambda.
todo_overwrite_not_implemented
.value_recursion_compiler
, we disallow overwritingtmc
we allow overwriting.bytecode
, we map reuses to fresh allocations.