blob: a2450be6d13b6d67fa2a5965fe6ecb65e32a2814 [file] [log] [blame]
// Copyright (c) 2016, 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 'dart:math';
import '../messages/codes.dart';
import 'value_kind.dart';
mixin StackChecker {
/// Used to report an internal error encountered in the stack listener.
Never internalProblem(Message message, int charOffset, Uri uri);
/// Checks that [value] matches the expected [kind].
///
/// Use this in assert statements like
///
/// assert(checkStackValue(uri, fileOffset, ValueKind.Token, value));
///
/// to document and validate the expected value kind.
bool checkStackValue(
Uri uri, int? fileOffset, ValueKind kind, Object? value) {
if (!kind.check(value)) {
String message = 'Unexpected value `${value}` (${value.runtimeType}). '
'Expected ${kind}.';
if (fileOffset != null) {
// If offset is available report and internal problem to show the
// parsed code in the output.
throw internalProblem(
new Message(const Code<String>('Internal error'),
problemMessage: message),
fileOffset,
uri);
} else {
throw message;
}
}
return true;
}
/// Returns the size of the stack.
int get stackHeight;
/// Returns the [index]th element on the stack from the top, i.e. the top of
/// the stack has index 0.
Object? lookupStack(int index);
/// Checks that [base] is a valid base stack height for a call to
/// [checkStackStateForAssert].
///
/// This can be used to initialize a stack base for subsequent calls to
/// [checkStackStateForAssert]. For instance:
///
/// int? stackBase;
/// // Set up the current stack height as the stack base.
/// assert(checkStackBaseState(
/// uri, fileOffset, stackBase = stackHeight));
/// ...
/// // Check that the stack is empty, relative to the stack base.
/// assert(checkStackState(
/// uri, fileOffset, [], base: stackBase));
///
/// or
///
/// int? stackBase;
/// // Assert that the current stack height is at least 4 and set
/// // the stack height - 4 up as the stack base.
/// assert(checkStackBaseState(
/// uri, fileOffset, stackBase = stackHeight - 4));
/// ...
/// // Check that the stack contains a single `Foo` element, relative to
/// // the stack base.
/// assert(checkStackState(
/// uri, fileOffset, [ValuesKind.Foo], base: stackBase));
///
bool checkStackBaseStateForAssert(Uri uri, int? fileOffset, int base) {
if (base < 0) {
_throwProblem(
uri,
fileOffset,
"Too few elements on stack. "
"Expected ${stackHeight - base}, found $stackHeight.");
}
return true;
}
/// Checks the top of the current stack against [kinds]. If a mismatch is
/// found, a top of the current stack is print along with the expected [kinds]
/// marking the frames that don't match, and throws an exception.
///
/// If [base] provided, it is used as the reference stack base height at
/// which the [kinds] are expected to occur. This allows for checking that
/// the stack is empty wrt. the stack base height.
///
/// Use this in assert statements like
///
/// assert(checkStackState(
/// uri, fileOffset, [ValueKind.Integer, ValueKind.StringOrNull]))
///
/// to document the expected stack and get earlier errors on unexpected stack
/// content.
bool checkStackStateForAssert(Uri uri, int? fileOffset, List<ValueKind> kinds,
{int? base}) {
String? heightError;
String? kindError;
bool success = true;
int stackShift = 0;
if (base != null) {
int relativeStackHeight = stackHeight - base;
if (relativeStackHeight < kinds.length) {
heightError = "Too few elements on stack. "
"Expected ${kinds.length}, found $relativeStackHeight.";
success = false;
} else if (relativeStackHeight > kinds.length) {
heightError = "Too many elements on stack. "
"Expected ${kinds.length}, found $relativeStackHeight.";
success = false;
}
// Shift the stack lookup indices so that [kinds] are checked relative
// to the stack base instead of relative to the top of the stack.
stackShift = relativeStackHeight - kinds.length;
} else {
if (stackHeight < kinds.length) {
heightError = "Too few elements on stack. "
"Expected ${kinds.length}, found $stackHeight.";
success = false;
}
}
for (int kindIndex = 0; kindIndex < kinds.length; kindIndex++) {
ValueKind kind = kinds[kindIndex];
int stackOffset = kindIndex + stackShift;
if (0 <= stackOffset && stackOffset < stackHeight) {
Object? value = lookupStack(stackOffset);
if (!kind.check(value)) {
kindError = "Unexpected element kind(s).";
success = false;
}
} else {
success = false;
}
}
if (!success) {
StringBuffer sb = new StringBuffer();
if (heightError != null) {
sb.writeln(' $heightError');
}
if (kindError != null) {
sb.writeln(' $kindError');
}
String safeToString(Object? object) {
try {
return '$object'.replaceAll('\r', '').replaceAll('\n', '');
} catch (e) {
// Judgments fail on toString.
return object.runtimeType.toString();
}
}
String padLeft(Object object, int length) {
String text = safeToString(object);
if (text.length < length) {
return ' ' * (length - text.length) + text;
}
return text;
}
String padRight(Object object, int length) {
String text = safeToString(object);
if (text.length < length) {
return text + ' ' * (length - text.length);
}
return text;
}
// Compute kind/stack frame information for all expected values plus 3 more
// stack elements if available.
int startIndex = min(-stackShift, 0);
int endIndex = max(kinds.length + stackShift, stackHeight);
for (int kindIndex = startIndex; kindIndex < endIndex + 3; kindIndex++) {
int stackOffset = kindIndex + stackShift;
if (stackOffset >= stackHeight && kindIndex > kinds.length) {
// No more stack elements nor kinds to display.
break;
}
if (kindIndex == kinds.length && base != null) {
// Show where the stack base is in the stack. Elements printed above
// this line are the checked/expected stack.
sb.write('>');
} else {
sb.write(' ');
}
if (stackOffset >= 0) {
sb.write(padLeft(stackOffset, 3));
} else {
sb.write(padLeft('*', 3));
}
sb.write(': ');
ValueKind? kind;
if (kindIndex < 0) {
sb.write(padRight('', 60));
} else if (kindIndex < kinds.length) {
kind = kinds[kindIndex];
sb.write(padRight(kind, 60));
} else {
sb.write(padRight('---', 60));
}
if (0 <= stackOffset && stackOffset < stackHeight) {
Object? value = lookupStack(stackOffset);
if (kind == null || kind.check(value)) {
sb.write(' ');
} else {
sb.write('*');
}
sb.write(safeToString(value));
sb.write(' (${value.runtimeType})');
} else {
if (kind == null) {
sb.write(' ');
} else {
sb.write('*');
}
sb.write('---');
}
sb.writeln();
}
_throwProblem(uri, fileOffset, sb.toString());
}
return success;
}
Never _throwProblem(Uri uri, int? fileOffset, String text) {
String message = '$runtimeType failure\n$text';
if (fileOffset != null) {
// If offset is available report and internal problem to show the
// parsed code in the output.
throw internalProblem(
new Message(const Code<String>('Internal error'),
problemMessage: message),
fileOffset,
uri);
} else {
throw message;
}
}
}