| // 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'; | |
| /// Checks that [ir] is well-formed. | |
| /// | |
| /// Throws [ValidationError] if it's not. | |
| /// | |
| /// During validation, progress information will be reported to [eventListener] | |
| /// (if provided). | |
| void validate(BaseIRContainer ir, {ValidationEventListener? eventListener}) { | |
| eventListener ??= ValidationEventListener(); | |
| var validator = _Validator(ir, eventListener: eventListener); | |
| eventListener._validator = validator; | |
| validator.run(); | |
| eventListener._validator = null; | |
| } | |
| class ValidationError extends Error { | |
| final int address; | |
| final String instructionString; | |
| final String message; | |
| ValidationError({ | |
| required this.address, | |
| required this.instructionString, | |
| required this.message, | |
| }); | |
| @override | |
| String toString() => | |
| 'Validation error at $address ($instructionString): $message'; | |
| } | |
| /// Event listener used by [validate] to report progress information. | |
| /// | |
| /// By itself this class does nothing; the caller of [validate] should make a | |
| /// derived class that overrides one or more of the `on...` methods. | |
| base class ValidationEventListener { | |
| late _Validator? _validator; | |
| int get localCount => _validator!.localCount; | |
| ValueCount get valueStackDepth => _validator!.valueStackDepth; | |
| /// Called when the validator has completely finished analyzing the | |
| /// instruction stream. | |
| void onFinished() {} | |
| /// Called prior to visiting each instruction. | |
| void onInstruction(int address) {} | |
| } | |
| /// A count of value entries. | |
| /// | |
| /// The count may be [indeterminate], indicating that the current control flow | |
| /// state is unreachable, and so exact matching of value counts is unnecessary. | |
| /// | |
| // TODO(paulberry): when extension types are supported, make this an extension | |
| // type. | |
| class ValueCount { | |
| static const indeterminate = ValueCount._(-1); | |
| final int _depth; | |
| ValueCount(this._depth) : assert(_depth >= 0); | |
| const ValueCount._(this._depth); | |
| @override | |
| int get hashCode => _depth.hashCode; | |
| ValueCount operator +(int delta) { | |
| assert(delta >= 0); | |
| if (this == indeterminate) return indeterminate; | |
| return ValueCount(_depth + delta); | |
| } | |
| ValueCount operator -(int delta) { | |
| assert(delta >= 0); | |
| if (this == indeterminate) return indeterminate; | |
| return ValueCount(_depth - delta); | |
| } | |
| @override | |
| bool operator ==(other) => other is ValueCount && _depth == other._depth; | |
| bool indeterminateOrAtLeast(int other) => | |
| this == indeterminate || _depth >= other; | |
| bool indeterminateOrEqualTo(int other) => | |
| this == indeterminate || _depth == other; | |
| @override | |
| String toString() => _depth.toString(); | |
| } | |
| /// Used by [_Validator] to track a control flow instruction (`block`, `loop`, | |
| /// `tryCatch`, `tryFinally`, or `function` instruction) whose `matching `end` | |
| /// instruction has not yet been encountered. | |
| class _ControlFlowElement { | |
| /// The state of [_Validator.localCount] before the control flow instruction | |
| /// was encountered. | |
| final int localCountBefore; | |
| /// The state of [_Validator.functionFlags] before the control flow | |
| /// instruction was encountered. | |
| final FunctionFlags functionFlagsBefore; | |
| /// The number of entries that will be in the value stack after the matching | |
| /// `end` instruction. | |
| final ValueCount valueStackDepthAfter; | |
| /// The number of values that will be consumed from the value stack when the | |
| /// control flow construct is ended (or branched out of). | |
| final int branchValueCount; | |
| /// Whether the control flow instruction was a `function` instruction. | |
| final bool isFunction; | |
| _ControlFlowElement({ | |
| required this.localCountBefore, | |
| required this.functionFlagsBefore, | |
| required this.valueStackDepthAfter, | |
| required this.branchValueCount, | |
| this.isFunction = false, | |
| }); | |
| } | |
| class _Validator { | |
| final BaseIRContainer ir; | |
| final ValidationEventListener eventListener; | |
| final controlFlowStack = <_ControlFlowElement>[]; | |
| var address = 0; | |
| /// Flags from the most recent `function` instruction whose corresponding | |
| /// `end` instruction has not yet been encountered. | |
| var functionFlags = FunctionFlags(); | |
| var localCount = 0; | |
| var valueStackDepth = ValueCount(0); | |
| _Validator(this.ir, {required this.eventListener}); | |
| /// Validates a `br` or `brIf` instruction. | |
| void branch(int nesting, {required bool conditional}) { | |
| if (conditional) popValues(1); | |
| check(nesting >= 0, 'Negative branch nesting'); | |
| var target = controlFlowStack.length - 1 - nesting; | |
| check(target >= 0, 'Control flow stack underflow'); | |
| for (var i = target + 1; i < controlFlowStack.length; i++) { | |
| check( | |
| !controlFlowStack[i].isFunction, | |
| 'Cannot branch outside of enclosing function', | |
| ); | |
| } | |
| var branchValueCount = controlFlowStack[target].branchValueCount; | |
| popValues(branchValueCount); | |
| if (conditional) { | |
| pushValues(branchValueCount); | |
| } else { | |
| valueStackDepth = ValueCount.indeterminate; | |
| } | |
| } | |
| /// Reports a validation error if [condition] is `false`. | |
| void check(bool condition, String message) { | |
| if (!condition) { | |
| fail(message); | |
| } | |
| } | |
| /// Unconditionally reports a validation error. | |
| Never fail(String message) { | |
| throw ValidationError( | |
| address: address, | |
| instructionString: address < ir.endAddress | |
| ? ir.instructionToString(address) | |
| : 'after last instruction', | |
| message: message, | |
| ); | |
| } | |
| void popValues(int count) { | |
| check( | |
| valueStackDepth.indeterminateOrAtLeast(count), | |
| 'Value stack underflow', | |
| ); | |
| valueStackDepth -= count; | |
| } | |
| void pushValues(int count) { | |
| valueStackDepth += count; | |
| } | |
| void run() { | |
| check(ir.endAddress > 0, 'No instructions'); | |
| for (address = 0; address < ir.endAddress; address++) { | |
| eventListener.onInstruction(address); | |
| var opcode = ir.opcodeAt(address); | |
| check( | |
| address != 0 || opcode == Opcode.function, | |
| 'First instruction must be function', | |
| ); | |
| switch (opcode) { | |
| case Opcode.alloc: | |
| var count = Opcode.alloc.decodeCount(ir, address); | |
| check(count >= 0, 'Negative alloc count'); | |
| localCount += count; | |
| case Opcode.await_: | |
| check(functionFlags.isAsync, 'Await in synchronous function'); | |
| popValues(1); | |
| pushValues(1); | |
| case Opcode.block: | |
| var inputCount = Opcode.block.decodeInputCount(ir, address); | |
| var outputCount = Opcode.block.decodeOutputCount(ir, address); | |
| check(inputCount >= 0, 'Negative input count'); | |
| check(outputCount >= 0, 'Negative output count'); | |
| popValues(inputCount); | |
| controlFlowStack.add( | |
| _ControlFlowElement( | |
| localCountBefore: localCount, | |
| functionFlagsBefore: functionFlags, | |
| valueStackDepthAfter: valueStackDepth + outputCount, | |
| branchValueCount: outputCount, | |
| ), | |
| ); | |
| valueStackDepth = ValueCount(inputCount); | |
| case Opcode.br: | |
| var nesting = Opcode.br.decodeNesting(ir, address); | |
| branch(nesting, conditional: false); | |
| case Opcode.brIf: | |
| var nesting = Opcode.brIf.decodeNesting(ir, address); | |
| branch(nesting, conditional: true); | |
| case Opcode.call: | |
| var argumentNames = Opcode.call.decodeArgumentNames(ir, address); | |
| popValues(ir.decodeArgumentNames(argumentNames).length); | |
| pushValues(1); | |
| case Opcode.concat: | |
| var count = Opcode.concat.decodeCount(ir, address); | |
| check(count >= 0, 'Negative concat count'); | |
| popValues(count); | |
| pushValues(1); | |
| case Opcode.drop: | |
| popValues(1); | |
| case Opcode.dup: | |
| popValues(1); | |
| pushValues(2); | |
| case Opcode.end: | |
| check(controlFlowStack.isNotEmpty, 'Unmatched end'); | |
| var controlFlowElement = controlFlowStack.removeLast(); | |
| check( | |
| localCount == controlFlowElement.localCountBefore, | |
| 'Unreleased locals', | |
| ); | |
| popValues(controlFlowElement.branchValueCount); | |
| check( | |
| valueStackDepth.indeterminateOrEqualTo(0), | |
| '${valueStackDepth._depth} superfluous value(s) remaining', | |
| ); | |
| valueStackDepth = controlFlowElement.valueStackDepthAfter; | |
| functionFlags = controlFlowElement.functionFlagsBefore; | |
| case Opcode.eq: | |
| popValues(2); | |
| pushValues(1); | |
| case Opcode.function: | |
| var type = Opcode.function.decodeType(ir, address); | |
| var kind = Opcode.function.decodeFlags(ir, address); | |
| check( | |
| !kind.isInstance || address == 0, | |
| 'Instance function may only be used at instruction address 0', | |
| ); | |
| controlFlowStack.add( | |
| _ControlFlowElement( | |
| localCountBefore: localCount, | |
| functionFlagsBefore: functionFlags, | |
| valueStackDepthAfter: valueStackDepth + 1, | |
| branchValueCount: 1, | |
| isFunction: true, | |
| ), | |
| ); | |
| functionFlags = kind; | |
| valueStackDepth = ValueCount( | |
| ir.countParameters(type) + (kind.isInstance ? 1 : 0), | |
| ); | |
| case Opcode.identical: | |
| popValues(2); | |
| pushValues(1); | |
| case Opcode.is_: | |
| popValues(1); | |
| pushValues(1); | |
| case Opcode.literal: | |
| pushValues(1); | |
| case Opcode.loop: | |
| var inputCount = Opcode.loop.decodeInputCount(ir, address); | |
| check(inputCount >= 0, 'Negative input count'); | |
| popValues(inputCount); | |
| controlFlowStack.add( | |
| _ControlFlowElement( | |
| localCountBefore: localCount, | |
| functionFlagsBefore: functionFlags, | |
| valueStackDepthAfter: ValueCount.indeterminate, | |
| branchValueCount: inputCount, | |
| ), | |
| ); | |
| valueStackDepth = ValueCount(0); | |
| pushValues(inputCount); | |
| case Opcode.not: | |
| popValues(1); | |
| pushValues(1); | |
| case Opcode.readLocal: | |
| var localIndex = Opcode.readLocal.decodeLocalIndex(ir, address); | |
| check(localIndex >= 0, 'Negative local index'); | |
| check(localIndex < localCount, 'No such local'); | |
| pushValues(1); | |
| case Opcode.release: | |
| var count = Opcode.release.decodeCount(ir, address); | |
| check(count >= 0, 'Negative release count'); | |
| var localCountFence = | |
| controlFlowStack.lastOrNull?.localCountBefore ?? 0; | |
| var newLocalCount = localCount - count; | |
| check( | |
| newLocalCount >= localCountFence, | |
| 'Local variable stack underflow', | |
| ); | |
| localCount = newLocalCount; | |
| case Opcode.shuffle: | |
| var popCount = Opcode.shuffle.decodePopCount(ir, address); | |
| var stackIndices = ir.decodeStackIndices( | |
| Opcode.shuffle.decodeStackIndices(ir, address), | |
| ); | |
| check(popCount >= 0, 'Negative pop count'); | |
| for (var stackIndex in stackIndices) { | |
| check(stackIndex >= 0, 'Negative stack index'); | |
| check(stackIndex < popCount, 'Stack index too large'); | |
| } | |
| popValues(popCount); | |
| pushValues(stackIndices.length); | |
| case Opcode.writeLocal: | |
| var localIndex = Opcode.writeLocal.decodeLocalIndex(ir, address); | |
| check(localIndex >= 0, 'Negative local index'); | |
| check(localIndex < localCount, 'No such local'); | |
| popValues(1); | |
| case Opcode.yield_: | |
| check(functionFlags.isGenerator, 'Yield in non-generator function'); | |
| popValues(1); | |
| default: | |
| fail('Unexpected opcode ${opcode.describe()}'); | |
| } | |
| } | |
| eventListener.onFinished(); | |
| check(controlFlowStack.isEmpty, 'Missing end'); | |
| } | |
| } |