blob: d9d532586aff475e3a73cb8147947b9e7b5159c8 [file] [log] [blame] [edit]
// 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');
}
}