| // 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; |
| } |
| } |
| } |