229d793602
Previously we tried to rely on the assumption that all variables would be boxed - so the machinery for setting correct catch-entry state only supported tagged values and constants. However this both leads to worse code and is not entirely correct assumption. This also: - renames various confusingly named classes: we move away from talking about "catch entry state" to "catch entry moves" - because we only record a subset of moves that needs to be performed and that does not describe the whole state; - refactors a bunch of associated code to be more readable and maintainable; - adds documentation about catch implementation in optimized code to runtime/docs/compiler; Fixes https://github.com/flutter/flutter/issues/21685. Change-Id: I03ae361a1bb7710acbd9f661ae014e663a163c59 Reviewed-on: https://dart-review.googlesource.com/74860 Commit-Queue: Vyacheslav Egorov <vegorov@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com> Reviewed-by: Alexander Markov <alexmarkov@google.com>
93 lines
4.6 KiB
Markdown
93 lines
4.6 KiB
Markdown
# Exceptions Implementation
|
|
|
|
This page describes how exceptions throwing and catching is implemented in the
|
|
VM.
|
|
|
|
## Intermediate Language
|
|
|
|
Dart VM's IL **does not** explicitly represent exceptional control flow in its
|
|
flow graph, there are **no** explicit exceptional edges connecting potentially
|
|
throwing instructions (e.g. calls) with corresponding catch blocks. Instead this
|
|
connection is defined at the block level: all exceptions that occur in any block
|
|
with the given `try_index` will be caught by `CatchBlockEntry` with the equal
|
|
`catch_try_index`.
|
|
|
|

|
|
|
|
For optimized code this means that data flow associated with exceptional control
|
|
flow is also represented implicitly: due to the absence of explicit exceptional
|
|
edges the data flow can't be represented using explicit phi-functions. Instead
|
|
in optimized code each `CatchBlockEntry` is treated almost as if it was an
|
|
independent entry into the function: for each variable `v` `CatchBlockEntry`
|
|
will contain a `Parameter(...)` instruction restoring variable state at catch
|
|
entry from a fixed location on the stack. When an exception is thrown runtime
|
|
system takes care of populating these stack slots with right values - current
|
|
state of corresponding local variables. It's easy to see a parallel between
|
|
these `Parameter(...)` instructions and `Phi(...)` instructions that would be
|
|
used if exception control flow would be explicit.
|
|
|
|

|
|
|
|
How does runtime system populate stack slots corresponding to these
|
|
`Parameter(...)` instructions? During compilation necessary information is
|
|
available in _deoptimization environment_ attached to the instruction. This
|
|
environment encodes the state of local variables in terms of SSA values i.e. if
|
|
we need to reconstruct unoptimized frame which SSA value should be stored into
|
|
the given local variable (see [Optimized
|
|
IL](compiler-pipeline-overview.md#optimized-il) for
|
|
an overview). However the way we use these information for exception handling is
|
|
slightly different in JIT and AOT modes.
|
|
|
|
### AOT mode
|
|
|
|
AOT mode does not support deoptimization and thus AOT compiler does not
|
|
associate any deoptimization metadata with generated code. Instead
|
|
deoptimization environments associated with instructions that can throw are
|
|
converted into `CatchEntryMoves` metadata during code generation and resulting
|
|
metadata is stored `RawCode::catch_entry_moves_maps_` in a compressed form.
|
|
|
|
`CatchEntryMoves` is essentially a sequence of moves which runtime needs to
|
|
perform to create the state that catch entry expects. There are three types of
|
|
moves:
|
|
|
|
* `*(FP + Dst) <- ObjectPool[PoolIndex]` - a move of a constant from an object
|
|
pool;
|
|
* `*(FP + Dst) <- *(FP + Src)` - a move of a tagged value;
|
|
* `*(FP + Dst) <- Box<Rep>(*(FP + Src))` - a boxing operation for an untagged
|
|
value;
|
|
|
|
When an exception is caught runtime decompresses the metadata associated with the
|
|
call site which has thrown an exception and uses it to prepare the state of the
|
|
stack for the catch block entry. See
|
|
`ExceptionHandlerFinder::{ReadCompressedCatchEntryMoves, ExecuteCatchEntryMoves}`.
|
|
|
|
NOTE: See [this
|
|
design/motivation](https://docs.google.com/a/google.com/document/d/1_vX8VkvHVA1Om7jjONiWLA325k_JmSZuvVClet-x-xM/edit?usp=sharing)
|
|
document for `CatchEntryMoves` metadata
|
|
|
|
### JIT mode
|
|
|
|
JIT mode heavily relies on deoptimization and all call instructions have (lazy)
|
|
deoptimization environments associated with them. These environments are
|
|
converted to [deoptimization
|
|
instructions](deoptimization.md#in-optimized-code)
|
|
during code generation and stored on the `Code` object.
|
|
|
|
When an exception is caught the runtime system converts deoptimization
|
|
environment associated with the call site that threw an exception into
|
|
`CatchEntryMoves` and then uses it to prepare the state of the stack for the
|
|
catch block entry. See `ExceptionHandlerFinder::{GetCatchEntryMovesFromDeopt, ExecuteCatchEntryMoves}`.
|
|
|
|
Constructing `CatchEntryMoves` dynamically from deoptimization instructions
|
|
allows to avoid unnecessary duplication of the metadata and save memory: as
|
|
deoptimization environments contain all information necessary for constructing
|
|
correct stack state.
|
|
|
|
IMPORTANT: There is a subtle difference between DBC and other architectures with
|
|
respect to catch block entry state. On normal architectures `Parameter(i)` at
|
|
catch entry would be associated with the same stack space that would be used to
|
|
store variable with index `i`. On DBC however at catch entry `Parameter(i)`
|
|
would be allocated to a separate scratch space at the very top of the register
|
|
space. See `FlowGraphAllocator::ProcessInitialDefinition` and
|
|
`FlowGraphCompiler::CatchEntryRegForVariable`.
|