Flat map statements into blocks, and use in Aslp#217
Conversation
plan: - writing isolate function is too hard? - flat_map : stmt -> (proc * id * id | block | stmt)
always include them
because we would need to compute the one *after* the group in order to know when to end the group. therefore, if we're in a bare statement group, we could still compute a block-pair ID before returning the bare statement group. i guess we would have to make it a Seq in the group lists too. but that seems hard to avoid re-computing. maybe just use persistent??
Seq cannot have foldmap
This reverts commit 58145f0.
…ent?" This reverts commit 778eef6.
Does this strictly insert two blocks from proc' into proc? Or does it take all blocks between these assuming a diamond? What happens if the caller makes unrelated changes to the procedure in Var.t, Var.t, Program.e) Stmt.t ->
( ID.t * ID.t * ('a, 'b) Procedure.t,
(Var.t, Var.t, Program.e) Stmt.t list )
Either.tThis should be a defined type or polymorphic variant with names for the cases. What is the |
|
I think I've done the suggestions now :) The functions are moved around, renamed, and better documented. |
| (** Zero or more straight-line statements. *) | ||
| | `Blocks of (Var.t, BasilExpr.t) Block.t list | ||
| (** Zero or more straight-line blocks. *) | ||
| | `Graph of ID.t * ID.t * ('v, 'e) t |
There was a problem hiding this comment.
If declaring the type it may as well be a regular variant as it provides better type inference and errors.
There was a problem hiding this comment.
Variants are kinda annoying to use because you have to qualify them with the module. Also, a variant like Procedure.Blocks is rather confusing I think, so it would need a different name anyway. And it's really just a special type used in this one function that is only declared separately so I can ocamldoc it.
Unless you feel strongly, I will leave it.
| (** [general_flat_map_stmts ~f bid proc] maps each statement in the given block | ||
| ID through [f]. For each statement, [f] may return either zero or more | ||
| "bare" statements, {i or} a first/last pair of new block-level control-flow. | ||
| See {!type-mapped_stmt} for details about [f]'s return type. |
There was a problem hiding this comment.
Or list straight-line list of blocks.
|
|
||
| Additionally, redirects the original block's incoming/outgoing edges to the | ||
| first / last block of the mapped output. *) | ||
| let general_flat_map_stmts ~(f : proc:_ t -> _ Stmt.t -> _ mapped_stmt) base_bid |
There was a problem hiding this comment.
redirects the original block's incoming/outgoing edges to the
first / last block of the mapped output
This seems subtle—trivial for Stmts, obvious for Blocks, but the Graph case could do odd things like remove the successors of the original block, or add additional successors. I think it should be made clear that the Graph case need not produce an additional subgraph that is connected to the original? Its just a new subgraph detached in the procedure (but in the same forest) that gets linked up by this function?
One or both of [first]/[last] may be the same as the original block.
Not sure if this case is more confusing to allow; might want to force to return blocks or stmts in that case.
Could a version of the API generate fresh begin + end blocks of the diamond and pass them to a closure returned by the Graph case to inject the diamond between? That way you would be appending / prepending to a block and have it be automatically bookended; it could check the succ/pred sets of the end points aren't modified. Maybe it could simplify the other special cases where it can concat to a prev block as well.
Sorry I say all this because its not obvious how I would use this immediately; e.g. what assumption do you make of the state of the graph within f when processing an arbitrary stmt of the program in order to modify the graph? And what do you have to guarantee about the graph you return?
Maybe the subgraph refactor would be the easiest way to clean that up though.
There was a problem hiding this comment.
That's cool but I'm probably not going to do that (have the flat_map implementation provide bookend blocks). I don't think it's a qualitative improvement over the current approach if we're still relying on Procedure to store the block-level control-flow.
This seems subtle ...
Sorry I say all this because its not obvious how I would use this immediately
f's purpose is to map one statement. So, f should make no assumptions at all on the state of the graph.
Are your questions adequately answered by the doc comment on `Graph? If not, how can it be improved? I think it answers all of your questions, just not in the same graph theory words.
| let general_flat_map_stmts ~(f : proc:_ t -> _ Stmt.t -> _ mapped_stmt) base_bid | ||
| proc = | ||
| let existing_bids = | ||
| lazy (iter_blocks proc |> Iter.map fst |> Iter.to_set (module IDSet)) |
There was a problem hiding this comment.
I didn't know it 🤯
| ~stmts:(CCVector.to_list stmts) ()) | ||
| proc bs | ||
| in | ||
| ((proc, None), List.map (fun x -> Either.Left (x, x)) bids) |
There was a problem hiding this comment.
What are Left and Right representing in this result list?
There was a problem hiding this comment.
moved into a local function and added a comment above that local function
| if not ID.(equal first base_bid) then | ||
| add_goto ~from:base_bid ~targets:[ first ] | ||
| else Fun.id | ||
| in |
There was a problem hiding this comment.
Might help clarify the API if you also require that f : Graph case does not modify the control flow of the original block.
| (snd %> Procedure.iter_blocks | ||
| %> Iter.flat_map (fun (_, b) -> | ||
| Iter.append (Block.read_vars_iter b) (Block.assigned_vars_iter b))) | ||
| %> Iter.filter Var.is_global |
There was a problem hiding this comment.
It should be a requirement that this should always be less than Program.global_vars. It should be covered by checks.ml.
There was a problem hiding this comment.
Program.global_vars filters the decls list, but here we need to get referenced variables which may or may not have been declared.
|
|
||
| (** Modify stmt list by creating a mutable copy of the underlying vector *) | ||
| let fmap_stmts_copy (f : (('a, 'a, 'b) Stmt.t, 'c) Vector.t -> unit) b = | ||
| let fmap_stmts_copy (f : (('a, 'a, 'b) Stmt.t, 'mut) Vector.t -> unit) b = |
There was a problem hiding this comment.
Should 'mut be the specifc mutable type defined in Vector rather than generic?
There was a problem hiding this comment.
no. in CCVector, rw and ro are two totally distinct types.
There was a problem hiding this comment.
I know that, I thought it was being passed a mutable vector specifically, but Vector.copy is shallow and just returns the same vector if its immutable.
IDSet.of_iter missing-record-field-pattern Or list straight-line list of blocks.
This adds a function for mapping a single statement into one or more blocks (or statements), while fixing up all the control flow.
The function
fshould return multiple blocks by returning a(first_id, last_id, proc')wherefirstandlastare new blocks whichfinserts into the updatedproc'. Alternatively, it may also return astmt_list. In this case, it is assumed that the procedure doesn't change.The implementation is a bit big but hopefully it's normal. As well as being useful for Aslp, I'm sure this will be useful in other cases too. For example, maybe for inlining procedure calls.
This PR also changes the Aslp frontend to make use of the new
flat_map_stmts. This leads to much simpler code. The Aslp transform no longer needs to split the statement list into before+intrin+after, since we can just handle one statement at a time. Impressively, there are very minimal changes to the expect test output.It also fixes the quadratic runtime of the current code in main. There's also a expect test to verify this. If you want, this can be deleted since timing-based tests can easily become flaky.
I have also left some TODO comments in the diff and want some advice on those. I'm also not sure about the name of the function, there's already a few functions with similar names..