Expand description

Managing the scope stack. The scopes are tied to lexical scopes, so as we descend the THIR, we push a scope on the stack, build its contents, and then pop it off. Every scope is named by a region::Scope.

SEME Regions

When pushing a new Scope, we record the current point in the graph (a basic block); this marks the entry to the scope. We then generate more stuff in the control-flow graph. Whenever the scope is exited, either via a break or return or just by fallthrough, that marks an exit from the scope. Each lexical scope thus corresponds to a single-entry, multiple-exit (SEME) region in the control-flow graph.

For now, we record the region::Scope to each SEME region for later reference (see caveat in next paragraph). This is because destruction scopes are tied to them. This may change in the future so that MIR lowering determines its own destruction scopes.

Not so SEME Regions

In the course of building matches, it sometimes happens that certain code (namely guards) gets executed multiple times. This means that the scope lexical scope may in fact correspond to multiple, disjoint SEME regions. So in fact our mapping is from one scope to a vector of SEME regions. Since the SEME regions are disjoint, the mapping is still one-to-one for the set of SEME regions that we’re currently in.

Also in matches, the scopes assigned to arms are not always even SEME regions! Each arm has a single region with one entry for each pattern. We manually manipulate the scheduled drops in this scope to avoid dropping things multiple times.

Drops

The primary purpose for scopes is to insert drops: while building the contents, we also accumulate places that need to be dropped upon exit from each scope. This is done by calling schedule_drop. Once a drop is scheduled, whenever we branch out we will insert drops of all those places onto the outgoing edge. Note that we don’t know the full set of scheduled drops up front, and so whenever we exit from the scope we only drop the values scheduled thus far. For example, consider the scope S corresponding to this loop:

loop {
    let x = ..;
    if cond { break; }
    let y = ..;
}

When processing the let x, we will add one drop to the scope for x. The break will then insert a drop for x. When we process let y, we will add another drop (in fact, to a subscope, but let’s ignore that for now); any later drops would also drop y.

Early exit

There are numerous “normal” ways to early exit a scope: break, continue, return (panics are handled separately). Whenever an early exit occurs, the method break_scope is called. It is given the current point in execution where the early exit occurs, as well as the scope you want to branch to (note that all early exits from to some other enclosing scope). break_scope will record the set of drops currently scheduled in a DropTree. Later, before in_breakable_scope exits, the drops will be added to the CFG.

Panics are handled in a similar fashion, except that the drops are added to the MIR once the rest of the function has finished being lowered. If a terminator can panic, call diverge_from(block) with the block containing the terminator block.

Breakable scopes

In addition to the normal scope stack, we track a loop scope stack that contains only loops and breakable blocks. It tracks where a break, continue or return should go to.

Structs

DropData 🔒
DropIdx 🔒
DropTree 🔒
A tree of drops that we have deferred lowering. It’s used for:
ExitScopes 🔒
Scope 🔒
Unwind 🔒

Enums

The target of an expression that breaks out of a scope
DropKind 🔒

Constants

ROOT_NODE 🔒

Traits

A trait that determined how DropTree creates its blocks and links to any entry nodes.

Functions

Builds drops for pop_scope and leave_top_scope.