Skip to content

Flat map statements into blocks, and use in Aslp#217

Merged
agle merged 39 commits into
mainfrom
fjdoisafda
Jul 15, 2026
Merged

Flat map statements into blocks, and use in Aslp#217
agle merged 39 commits into
mainfrom
fjdoisafda

Conversation

@katrinafyi

@katrinafyi katrinafyi commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

This adds a function for mapping a single statement into one or more blocks (or statements), while fixing up all the control flow.

type ('v, 'e) mapped_stmt =
  [ `Stmts of (Var.t, Var.t, BasilExpr.t) Stmt.t list
  | `Blocks of (Var.t, BasilExpr.t) Block.t list
  | `Graph of ID.t * ID.t * ('v, 'e) t ]

let general_flat_map_stmts
    ~(f : proc:_ t -> _ Stmt.t -> _ mapped_stmt)
    base_bid
    proc

The function f should return multiple blocks by returning a (first_id, last_id, proc') where first and last are new blocks which f inserts into the updated proc'. Alternatively, it may also return a stmt_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..

@katrinafyi
katrinafyi requested a review from agle as a code owner July 10, 2026 07:39
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
@agle

agle commented Jul 13, 2026

Copy link
Copy Markdown
Owner

The function f should return multiple blocks by returning a Left (first_id, last_id, proc') where first and last are new blocks which f inserts into the updated proc'.

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 ~f?

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.t

This should be a defined type or polymorphic variant with names for the cases.

What is the base_bid, last ID.t argument?

@katrinafyi

Copy link
Copy Markdown
Collaborator Author

I think I've done the suggestions now :) The functions are moved around, renamed, and better documented.

Comment thread lib/lang/procedure.ml
(** 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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If declaring the type it may as well be a regular variant as it provides better type inference and errors.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/lang/procedure.ml Outdated
(** [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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or list straight-line list of blocks.

Comment thread lib/lang/procedure.ml Outdated
Comment thread lib/lang/procedure.ml Outdated

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@katrinafyi katrinafyi Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/lang/procedure.ml Outdated
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))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not IDSet.of_iter?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know it 🤯

Comment thread lib/lang/procedure.ml Outdated
Comment thread lib/lang/procedure.ml Outdated
~stmts:(CCVector.to_list stmts) ())
proc bs
in
((proc, None), List.map (fun x -> Either.Left (x, x)) bids)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are Left and Right representing in this result list?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved into a local function and added a comment above that local function

Comment thread lib/lang/procedure.ml
if not ID.(equal first base_bid) then
add_goto ~from:base_bid ~targets:[ first ]
else Fun.id
in

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might help clarify the API if you also require that f : Graph case does not modify the control flow of the original block.

Comment thread lib/lang/program.ml
(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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be a requirement that this should always be less than Program.global_vars. It should be covered by checks.ml.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Program.global_vars filters the decls list, but here we need to get referenced variables which may or may not have been declared.

Comment thread lib/lang/block.ml Outdated

(** 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 =

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should 'mut be the specifc mutable type defined in Vector rather than generic?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no. in CCVector, rw and ro are two totally distinct types.

@agle agle Jul 15, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

katrinafyi and others added 2 commits July 14, 2026 17:25
IDSet.of_iter
missing-record-field-pattern
Or list straight-line list of blocks.
@agle
agle enabled auto-merge (squash) July 15, 2026 02:55
@agle
agle merged commit 52212f3 into main Jul 15, 2026
9 checks passed
@agle
agle deleted the fjdoisafda branch July 15, 2026 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants