catchEnvironments property
The child environment each catch clause's body runs in, by clause.
SCE79. _handleAsyncError used to define the exception variable in the
FUNCTION's environment — its own comment said "can cause collisions" —
so in an async function the variable outlived its block and, worse,
silently overwrote a caller's local of the same name. e is one of the
most common names there is, and nothing threw or logged: the value was
simply wrong from that point on. visitTryStatement has always created a
child environment, which is what Dart requires.
WHY A MAP AND NOT A STACK, which is the design question this posed. A stack has to be POPPED, and the state machine resumes at a node rather than executing a block — so every exit from a catch block (fallthrough, return, rethrow, break, an error) would need to pop, and one missed exit leaks the scope again in a way nothing would notice. Keyed by clause, the binding is SELECTED structurally instead: a node lexically inside the clause gets the environment, a node outside it never asks. There is no exit to instrument because there is nothing to undo.
A re-entered clause overwrites its entry, so a try inside a loop gets a fresh environment per iteration rather than resuming a stale one.
KEYED BY IDENTITY, and load-bearing in the tom_d4rt_ast twin for the
same reason doBodiesStarted records: SAstNode overrides == with a
structural toJson() comparison, so a plain Map there would treat two
identical-looking catch clauses as one.
Implementation
final Map<SCatchClause, Environment> catchEnvironments =
Map<SCatchClause, Environment>.identity();