| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analyzer/src/wolf/ir/ir.dart'; |
| |
| /// Analyzes the scopes in a [BaseIRContainer]. |
| /// |
| /// This function computes the nesting of begin/end scopes in [ir]. A begin/end |
| /// scope is the set of instructions between a "begin" instruction (a `block`, |
| /// `loop`, `tryCatch`, `tryFinally`, `function`, or `instanceFunction` |
| /// instruction) and the `end` instruction that matches it. |
| Scopes analyzeScopes(BaseIRContainer ir, |
| {ScopeAnalyzerEventListener? eventListener}) { |
| eventListener ??= ScopeAnalyzerEventListener(); |
| var scopeAnalyzer = _ScopeAnalyzer(ir, eventListener); |
| eventListener._scopeAnalyzer = scopeAnalyzer; |
| scopeAnalyzer.run(); |
| eventListener._scopeAnalyzer = null; |
| return Scopes._(scopeAnalyzer); |
| } |
| |
| /// Event listener used by [analyzeScopes] to report progress information. |
| /// |
| /// By itself this class does nothing; the caller of [analyzeScopes] should make |
| /// a derived class that overrides one or more of the `on...` methods. |
| base class ScopeAnalyzerEventListener { |
| /// When an invocation of [analyzeScopes] is running and using this listener, |
| /// the [_ScopeAnalyzer] that is performing the analysis; otherwise `null`. |
| _ScopeAnalyzer? _scopeAnalyzer; |
| |
| /// Allocates a fresh state variable. |
| /// |
| /// Clients of the scope analyzer can use this method to allocate state |
| /// variables to track pieces of state that are specific to a particular kind |
| /// of analysis (e.g., a state variable could be used to track the number of |
| /// elements in a list that's referred to by the code being analyzed, or to |
| /// track the state of the event loop in the case of asynchronous code). |
| StateVar createStateVar() => _scopeAnalyzer!.createStateVar(); |
| |
| /// Called for each variable allocated by an `alloc` instruction. |
| /// |
| /// [index] is the index of the allocated variable (counting from zero at the |
| /// start of the code being analyzed; every call to this method within the |
| /// context of a single call to [analyzeScopes] will an index one greater than |
| /// the previous call). |
| /// |
| /// [stateVar] is the state variable that the scope analyzer has allocated to |
| /// track the value of the variable. |
| void onAlloc({required int index, required StateVar stateVar}) {} |
| |
| /// Called when the scope analyzer has completely finished analyzing the |
| /// instruction stream. |
| void onFinished() {} |
| |
| /// Called prior to visiting each instruction. |
| /// |
| /// This call back is invoked before more specific callbacks like [onAlloc]. |
| void onInstruction(int address) {} |
| |
| /// Called when [scopeAnalyzer] is about to process a "begin" instruction. |
| void onPushScope({required int address, required int scope}) {} |
| |
| /// Records that the value of a state variable was potentially affected. |
| /// |
| /// Clients of the scope analyzer can use this method to indicate that one of |
| /// the state variables returned by [createStateVar] was affected by a later |
| /// instruction. |
| void stateVarAffected(StateVar stateVar) => |
| _scopeAnalyzer!.stateVarAffected(stateVar); |
| } |
| |
| /// The result of scope analysis. |
| /// |
| /// See [analyzeScopes] for more information. |
| class Scopes { |
| /// Total number of state variables that were allocated. |
| /// |
| /// Valid values of [StateVar.index] range from `0` to `stateVarCount-1`. |
| final int stateVarCount; |
| |
| final List<StateVar> _allocIndexToStateVar; |
| final List<int> _beginAddresses; |
| final List<StateVar> _effects; |
| final List<int> _effectCounts; |
| final List<int> _effectIndices; |
| final List<int> _endAddresses; |
| final List<int> _lastDescendants; |
| final List<int> _parents; |
| |
| Scopes._(_ScopeAnalyzer analyzer) |
| : stateVarCount = analyzer.stateVarToPendingEffectsIndex.length, |
| _allocIndexToStateVar = analyzer.allocIndexToStateVar, |
| _beginAddresses = analyzer.beginAddresses, |
| _effects = analyzer.effects, |
| _effectCounts = analyzer.effectCounts, |
| _effectIndices = analyzer.effectIndices, |
| _endAddresses = analyzer.endAddresses, |
| _lastDescendants = analyzer.lastDescendants, |
| _parents = analyzer.parents; |
| |
| /// The number of scopes that was found. |
| int get scopeCount => _beginAddresses.length; |
| |
| /// The state variable tracking the [allocIndex]th local variable that was |
| /// allocated. |
| StateVar allocIndexToStateVar(int allocIndex) => |
| _allocIndexToStateVar[allocIndex]; |
| |
| /// Computes the innermost ancestor of [scope] (or [scope] itself), whose |
| /// [endAddress] is greater than or equal to [address]. |
| /// |
| /// This method may be used in conjunction with [lastScopeBefore] to find the |
| /// innermost scope containing an instruction, according to the formula: |
| /// |
| /// var scope = ancestorContainingAddress( |
| /// scope: lastScopeBefore(address), address: address); |
| int ancestorContainingAddress({required int scope, required int address}) { |
| while (endAddress(scope) < address) { |
| // By validation, we know that the instruction sequence begins with |
| // `function` or `instanceFunction`, so the outermost scope covers the |
| // whole instruction sequence. Therefore, if `scope` is `0`, that means |
| // that `address` must have been an invalid address. |
| assert(scope > 0, 'invalid address'); |
| scope = parent(scope); |
| } |
| return scope; |
| } |
| |
| /// The address of the "begin" instruction that opens [scope]. |
| /// |
| /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the |
| /// scope opened by the `i`th begin instruction in the IR. |
| int beginAddress(int scope) => _beginAddresses[scope]; |
| |
| /// Retrieves one of the state variables affected by a scope. |
| /// |
| /// To find the `i`th state variable affected by `scope`, use an [index] value |
| /// of `effectIndex(scope) + i`. |
| StateVar effectAt(int index) => _effects[index]; |
| |
| /// The number of state variables affected by instructions in [scope]. |
| /// |
| /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the |
| /// scope opened by the `i`th begin instruction in the IR. |
| int effectCount(int scope) => _effectCounts[scope]; |
| |
| /// The first index to pass to [effectAt] to enumerate the state variables |
| /// affected by instructions in [scope]. |
| /// |
| /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the |
| /// scope opened by the `i`th begin instruction in the IR. |
| int effectIndex(int scope) => _effectIndices[scope]; |
| |
| /// The address of the `end` instruction that closes [scope]. |
| /// |
| /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the |
| /// scope opened by the `i`th begin instruction in the IR. |
| int endAddress(int scope) => _endAddresses[scope]; |
| |
| /// The scope index of the last scope transitively contained within [scope]. |
| /// |
| /// If [scope] doesn't contain any other scopes, then [scope] is returned. |
| int lastDescendant(int scope) => _lastDescendants[scope]; |
| |
| /// Computes the highest-numbered scope whose [beginAddress] is less than or |
| /// equal to [address]. |
| /// |
| /// Scopes are numbered in pre-order, so the returned scope will either be the |
| /// innermost scope containing [address], or one of its ancestors will be (see |
| /// [ancestorContainingAddress]). |
| int mostRecentScope(int address) { |
| assert(address >= 0); |
| // By validation, we know that the instruction sequence begins with |
| // `function` or `instanceFunction`, so the outermost scope covers the whole |
| // instruction sequence. |
| assert(beginAddress(0) == 0); |
| var low = 0; |
| var high = scopeCount; |
| while (low < high - 1) { |
| // Loop invariants |
| assert(beginAddress(low) <= address); |
| assert(high == scopeCount || beginAddress(high) > address); |
| |
| var mid = (low + high) ~/ 2; |
| if (beginAddress(mid) <= address) { |
| low = mid; |
| } else { |
| high = mid; |
| } |
| } |
| return low; |
| } |
| |
| /// The scope immediately containing [scope], or `-1` if [scope] is the |
| /// outermost scope. |
| /// |
| /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the |
| /// scope opened by the `i`th begin instruction in the IR. |
| int parent(int scope) => _parents[scope]; |
| } |
| |
| /// A state variable tracked by the scope analyzer. |
| /// |
| /// State variables are simply unique integer indices; clients that need to |
| /// track information about each state variable should store that information in |
| /// a list indexed by the state variable. |
| // TODO(paulberry): when extension types are supported, make this an extension |
| // type. |
| class StateVar { |
| final int index; |
| |
| StateVar(this.index); |
| |
| @override |
| int get hashCode => index.hashCode; |
| |
| @override |
| bool operator ==(other) => other is StateVar && index == other.index; |
| |
| @override |
| String toString() => index.toString(); |
| } |
| |
| /// Analyzer of the scopes, and their effects on state variables, in a |
| /// [BaseIRContainer]. |
| /// |
| /// This class computes the nesting of begin/end scopes in [ir]. A begin/end |
| /// scope is the set of instructions between a "begin" instruction (a `block`, |
| /// `loop`, `tryCatch`, `tryFinally`, `function`, or `instanceFunction` |
| /// instruction) and the `end` instruction that matches it. It also computes the |
| /// set of state variables affected by the code within each begin/end scope. |
| /// |
| /// State variables correspond to local variables in the program, as well as any |
| /// other significant program state that needs to be tracked; the specific state |
| /// variables that are tracked are determined by caller of [analyzeScopes]. |
| base class _ScopeAnalyzer { |
| static const enableDebugPrints = false; |
| final BaseIRContainer ir; |
| final ScopeAnalyzerEventListener eventListener; |
| |
| /// Stack of scope indices for all open scopes. |
| /// |
| /// This stack begins with a sentinel value of `-1`, so that when [pushScope] |
| /// is called for the first time (to create the outermost scope) it will set |
| /// the outermost scope's parent scope to `-1`. |
| final scopeIndices = [-1]; |
| |
| /// Stack of state variables affected by all open scopes. |
| /// |
| /// The locals written in the outermost scope appear first, then the ones in |
| /// the next inner scope, and so on. Locals are de-duplicated within a scope. |
| final pendingEffects = <StateVar>[]; |
| |
| /// Stack of indices into [pendingEffects] representing the start of each |
| /// scope enclosing the current scope. |
| final outerScopeStarts = <int>[]; |
| |
| /// Table mapping each state variable to the index of its last appearance in |
| /// [pendingEffects]. |
| /// |
| /// And index of -1 means the local does not appear in [pendingEffects]. |
| final stateVarToPendingEffectsIndex = <int>[]; |
| |
| /// Table mapping each index in [pendingEffects] to the previous occurrence |
| /// of the same state variable in [pendingEffects]. |
| final previousPendingEffectsIndices = <int>[]; |
| |
| /// Table mapping each local to the state variable that tracks it. |
| final localToStateVar = <StateVar>[]; |
| |
| /// See [Scopes.allocIndexToStateVar]. |
| final allocIndexToStateVar = <StateVar>[]; |
| |
| /// See [Scopes.beginAddress]. |
| final beginAddresses = <int>[]; |
| |
| /// See [Scopes.effectAt]. |
| final effects = <StateVar>[]; |
| |
| /// See [Scopes.effectCount]. |
| final effectCounts = <int>[]; |
| |
| /// See [Scopes.effectIndex]. |
| final effectIndices = <int>[]; |
| |
| /// See [Scopes.endAddress]. |
| final endAddresses = <int>[]; |
| |
| /// See [Scopes.lastDescendant]. |
| final lastDescendants = <int>[]; |
| |
| /// See [Scopes.parent]. |
| final parents = <int>[]; |
| |
| var nextAllocIndex = 0; |
| |
| /// Index into [pendingEffects] representing the start of the current scope. |
| var scopeStart = 0; |
| |
| _ScopeAnalyzer(this.ir, this.eventListener); |
| |
| bool checkState() { |
| if (enableDebugPrints) dumpState(); |
| var stateVarCount = stateVarToPendingEffectsIndex.length; |
| var pendingEffectsCount = pendingEffects.length; |
| // `_scopeIndices` always starts with the sentinel value `-1`. |
| assert(scopeIndices[0] == -1, 'invalid sentinel value in _scopeIndices'); |
| // Scopes are numbered in pre-order, so `_scopeIndices` should be |
| // monotonically increasing. |
| for (var i = 0; i < scopeIndices.length - 1; i++) { |
| assert(scopeIndices[i] < scopeIndices[i + 1], |
| '_scopeIndices out of order: $scopeIndices'); |
| } |
| // State variables mentioned in `_pendingEffects` must exist. |
| for (var stateVar in pendingEffects) { |
| assert( |
| stateVar.index >= 0 && stateVar.index < stateVarCount, |
| '_pendingEffects contains non-existent state var $stateVar: ' |
| '$pendingEffects'); |
| } |
| // `_scopeStart` and `_outerScopeStarts` should refer to properly nested |
| // indices within `_pendingEffects`. |
| var previousScopeStart = 0; |
| var scopeStarts = [...outerScopeStarts, scopeStart]; |
| for (var scopeStart in scopeStarts) { |
| assert(scopeStart >= previousScopeStart, |
| 'improper nesting of scope starts: $scopeStarts'); |
| assert(scopeStart <= pendingEffectsCount, |
| 'scope start is too large: $scopeStart > $pendingEffectsCount'); |
| } |
| // `_stateVarToPendingEffectsIndex` and `_previousPendingEffectsIndices` |
| // should properly reflect the contents of `_pendingEffects`. |
| assert( |
| previousPendingEffectsIndices.length == pendingEffectsCount, |
| '_previousPendingEffectsIndices should have length ' |
| '$pendingEffectsCount: $previousPendingEffectsIndices'); |
| var expectedStateVarToPendingEffectsIndex = |
| List.filled(stateVarToPendingEffectsIndex.length, -1); |
| for (var i = 0; i < pendingEffectsCount; i++) { |
| assert( |
| previousPendingEffectsIndices[i] == |
| expectedStateVarToPendingEffectsIndex[pendingEffects[i].index], |
| '_previousPendingEffectsIndices[$i] is incorrect: expected ' |
| '${expectedStateVarToPendingEffectsIndex[pendingEffects[i].index]}, ' |
| 'got ${previousPendingEffectsIndices[i]}'); |
| expectedStateVarToPendingEffectsIndex[pendingEffects[i].index] = i; |
| } |
| for (var i = 0; i < stateVarCount; i++) { |
| assert( |
| stateVarToPendingEffectsIndex[i] == |
| expectedStateVarToPendingEffectsIndex[i], |
| '_stateVarToPendingEffectsIndex is incorrect: ' |
| 'expected $expectedStateVarToPendingEffectsIndex, ' |
| 'got $stateVarToPendingEffectsIndex'); |
| } |
| // State variables mentioned in `_localToStateVar` must exist. |
| for (var stateVar in localToStateVar) { |
| assert( |
| stateVar.index >= 0 && stateVar.index < stateVarCount, |
| '_localToStateVar contains non-existent state var $stateVar: ' |
| '$localToStateVar'); |
| } |
| return true; |
| } |
| |
| StateVar createStateVar() { |
| var stateVar = StateVar(stateVarToPendingEffectsIndex.length); |
| stateVarToPendingEffectsIndex.add(-1); |
| return stateVar; |
| } |
| |
| void dumpState() { |
| print(' scopeIndices: $scopeIndices'); |
| print(' pendingEffects: $pendingEffects'); |
| print(' scopeStart: $scopeStart'); |
| print(' outerScopeStarts: $outerScopeStarts'); |
| print(' stateVarToPendingEffectsIndex: $stateVarToPendingEffectsIndex'); |
| print(' previousPendingEffectsIndices: $previousPendingEffectsIndices'); |
| print(' localToStateVar: $localToStateVar'); |
| print(' allocIndexToStateVar: $allocIndexToStateVar'); |
| print(' beginAddresses: $beginAddresses'); |
| print(' effects: $effects'); |
| print(' effectCounts: $effectCounts'); |
| print(' effectIndices: $effectIndices'); |
| print(' endAddresses: $endAddresses'); |
| print(' lastDescendants: $lastDescendants'); |
| print(' parents: $parents'); |
| } |
| |
| void popScope(int address) { |
| var innerScopeStart = scopeStart; |
| scopeStart = outerScopeStarts.removeLast(); |
| var scopeIndex = scopeIndices.removeLast(); |
| endAddresses[scopeIndex] = address; |
| lastDescendants[scopeIndex] = lastDescendants.length - 1; |
| effectIndices[scopeIndex] = effects.length; |
| effectCounts[scopeIndex] = pendingEffects.length - innerScopeStart; |
| for (var i = innerScopeStart; i < pendingEffects.length;) { |
| var stateVar = pendingEffects[i]; |
| effects.add(stateVar); |
| var previousPendingEffectsIndex = previousPendingEffectsIndices[i]; |
| if (previousPendingEffectsIndex >= scopeStart) { |
| // This state variable was also affected by the outer scope, so remove |
| // the duplicate. |
| removePendingEffect(stateVar, i, previousPendingEffectsIndex); |
| // And then carry on with index `i`, which now represents a different |
| // state variable. |
| } else { |
| // This state variable effect wasn't duplicated, so move onto the next |
| // one. |
| i++; |
| } |
| } |
| } |
| |
| void pushScope(int address) { |
| var scope = beginAddresses.length; |
| eventListener.onPushScope(address: address, scope: scope); |
| var pendingEffectsLength = pendingEffects.length; |
| outerScopeStarts.add(scopeStart); |
| scopeStart = pendingEffectsLength; |
| var outerScope = scopeIndices.last; |
| scopeIndices.add(scope); |
| beginAddresses.add(address); |
| endAddresses.add(-1); |
| effectIndices.add(-1); |
| effectCounts.add(-1); |
| lastDescendants.add(-1); |
| parents.add(outerScope); |
| } |
| |
| void removePendingEffect(StateVar stateVar, int pendingEffectsIndex, |
| int previousPendingEffectsIndex) { |
| assert( |
| stateVarToPendingEffectsIndex[stateVar.index] == pendingEffectsIndex); |
| assert(previousPendingEffectsIndices[pendingEffectsIndex] == |
| previousPendingEffectsIndex); |
| // There may be additional entries in `_pendingEffects` after the entry to |
| // be removed, so move the last entry in `_pendingEffects` to |
| // `pendingEffectsIndex`. This is a no-op if the entry to be removed is |
| // already the last entry, but we do it anyway to avoid a |
| // difficult-to-predict branch. |
| var stateVarToMove = pendingEffects.last; |
| pendingEffects[pendingEffectsIndex] = stateVarToMove; |
| stateVarToPendingEffectsIndex[stateVarToMove.index] = pendingEffectsIndex; |
| previousPendingEffectsIndices[pendingEffectsIndex] = |
| previousPendingEffectsIndices.last; |
| // Drop the last entry in `_pendingEffects`. |
| stateVarToPendingEffectsIndex[stateVar.index] = previousPendingEffectsIndex; |
| pendingEffects.removeLast(); |
| previousPendingEffectsIndices.removeLast(); |
| } |
| |
| void run() { |
| for (var address = 0; address < ir.endAddress; address++) { |
| assert(checkState()); |
| if (enableDebugPrints) { |
| print('$address: ${ir.instructionToString(address)}'); |
| } |
| eventListener.onInstruction(address); |
| switch (ir.opcodeAt(address)) { |
| case Opcode.alloc: |
| var count = Opcode.alloc.decodeCount(ir, address); |
| for (var i = 0; i < count; i++) { |
| var stateVar = createStateVar(); |
| eventListener.onAlloc( |
| index: allocIndexToStateVar.length, stateVar: stateVar); |
| localToStateVar.add(stateVar); |
| allocIndexToStateVar.add(stateVar); |
| } |
| case Opcode.block: |
| case Opcode.function: |
| case Opcode.loop: |
| pushScope(address); |
| case Opcode.end: |
| popScope(address); |
| case Opcode.release: |
| var count = Opcode.release.decodeCount(ir, address); |
| for (var i = 0; i < count; i++) { |
| var stateVar = localToStateVar.last; |
| var pendingEffectsIndex = |
| stateVarToPendingEffectsIndex[stateVar.index]; |
| if (pendingEffectsIndex >= 0) { |
| // Local should have been allocated in this scope, so it can't |
| // affect any outer scope, and therefore we can stop tracking it. |
| const previousPendingEffectsIndex = -1; |
| removePendingEffect( |
| stateVar, pendingEffectsIndex, previousPendingEffectsIndex); |
| } |
| localToStateVar.removeLast(); |
| } |
| case Opcode.writeLocal: |
| var local = Opcode.writeLocal.decodeLocalIndex(ir, address); |
| stateVarAffected(localToStateVar[local]); |
| } |
| } |
| assert(checkState()); |
| assert(scopeIndices.length == 1); |
| assert(scopeStart == 0); |
| assert(outerScopeStarts.isEmpty); |
| assert(localToStateVar.isEmpty); |
| eventListener.onFinished(); |
| } |
| |
| void stateVarAffected(StateVar stateVar) { |
| var pendingEffectsIndex = stateVarToPendingEffectsIndex[stateVar.index]; |
| // Only record an entry in `_pendingEffects` if there isn't already an |
| // entry indicating that the state variable was affected in the |
| // current scope. |
| if (stateVarToPendingEffectsIndex[stateVar.index] < scopeStart) { |
| previousPendingEffectsIndices.add(pendingEffectsIndex); |
| stateVarToPendingEffectsIndex[stateVar.index] = pendingEffects.length; |
| pendingEffects.add(stateVar); |
| } |
| } |
| } |