diff --git a/runtime/lib/errors.cc b/runtime/lib/errors.cc index 27770a92f19..d10df392e2d 100644 --- a/runtime/lib/errors.cc +++ b/runtime/lib/errors.cc @@ -20,7 +20,7 @@ static RawScript* FindScript(DartFrameIterator* iterator) { // the inlining meta-data so we cannot walk the inline-aware stack trace. // Second, the script text itself is missing so whatever script is returned // from here will be missing the assertion expression text. - iterator->NextFrame(); // Skip _AssertionError._checkAssertion frame + iterator->NextFrame(); // Skip _AssertionError._evaluateAssertion frame return Exceptions::GetCallerScript(iterator); } StackFrame* stack_frame = iterator->NextFrame(); @@ -62,8 +62,9 @@ static RawScript* FindScript(DartFrameIterator* iterator) { // Allocate and throw a new AssertionError. // Arg0: index of the first token of the failed assertion. // Arg1: index of the first token after the failed assertion. +// Arg2: Message object or null. // Return value: none, throws an exception. -DEFINE_NATIVE_ENTRY(AssertionError_throwNew, 2) { +DEFINE_NATIVE_ENTRY(AssertionError_throwNew, 3) { // No need to type check the arguments. This function can only be called // internally from the VM. const TokenPosition assertion_start = @@ -71,7 +72,8 @@ DEFINE_NATIVE_ENTRY(AssertionError_throwNew, 2) { const TokenPosition assertion_end = TokenPosition(Smi::CheckedHandle(arguments->NativeArgAt(1)).Value()); - const Array& args = Array::Handle(Array::New(4)); + const Instance& message = Instance::CheckedHandle(arguments->NativeArgAt(2)); + const Array& args = Array::Handle(Array::New(5)); DartFrameIterator iterator; iterator.NextFrame(); // Skip native call. @@ -92,6 +94,7 @@ DEFINE_NATIVE_ENTRY(AssertionError_throwNew, 2) { args.SetAt(1, String::Handle(script.url())); args.SetAt(2, Smi::Handle(Smi::New(from_line))); args.SetAt(3, Smi::Handle(Smi::New(script.HasSource() ? from_column : -1))); + args.SetAt(4, message); Exceptions::ThrowByType(Exceptions::kAssertion, args); UNREACHABLE(); diff --git a/runtime/lib/errors_patch.dart b/runtime/lib/errors_patch.dart index 26358027749..6bd0881471a 100644 --- a/runtime/lib/errors_patch.dart +++ b/runtime/lib/errors_patch.dart @@ -18,29 +18,37 @@ class _AssertionError extends Error implements AssertionError { _AssertionError._create( - this._failedAssertion, this._url, this._line, this._column); + this._failedAssertion, this._url, this._line, this._column, + this.message); - static _throwNew(int assertionStart, int assertionEnd) - native "AssertionError_throwNew"; - static void _checkAssertion(condition, int start, int end) { + // AssertionError_throwNew in errors.cc fishes the assertion source code + // out of the script. It expects a Dart stack frame from class + // _AssertionError. Thus we need a Dart stub that calls the native code. + static _throwNew(int assertionStart, int assertionEnd, Object message) { + _doThrowNew(assertionStart, assertionEnd, message); + } + + static _doThrowNew(int assertionStart, int assertionEnd, Object message) + native "AssertionError_throwNew"; + + static _evaluateAssertion(condition) { if (condition is Function) { condition = condition(); } - if (!condition) { - _throwNew(start, end); - } + return condition; } - static void _checkConstAssertion(bool condition, int start, int end) { - if (!condition) { - _throwNew(start, end); - } + String get _messageString { + if (message == null) return "is not true."; + if (message is String) return message; + return Error.safeToString(message); } String toString() { if (_url == null) { - return _failedAssertion; + if (message == null) return _failedAssertion; + return "'$_failedAssertion': $_messageString"; } var columnInfo = ""; if (_column > 0) { @@ -48,17 +56,18 @@ class _AssertionError extends Error implements AssertionError { columnInfo = " pos $_column"; } return "'$_url': Failed assertion: line $_line$columnInfo: " - "'$_failedAssertion' is not true."; + "'$_failedAssertion': $_messageString"; } final String _failedAssertion; final String _url; final int _line; final int _column; + final Object message; } class _TypeError extends _AssertionError implements TypeError { - _TypeError._create(String url, int line, int column, this._errorMsg) - : super._create("is assignable", url, line, column); + _TypeError._create(String url, int line, int column, String errorMsg) + : super._create("is assignable", url, line, column, errorMsg); static _throwNew(int location, Object src_value, @@ -78,9 +87,7 @@ class _TypeError extends _AssertionError implements TypeError { } } - String toString() => _errorMsg; - - final String _errorMsg; + String toString() => super.message; } class _CastError extends Error implements CastError { diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h index 20861868410..1260f7cd426 100644 --- a/runtime/vm/bootstrap_natives.h +++ b/runtime/vm/bootstrap_natives.h @@ -159,7 +159,7 @@ namespace dart { V(DateTime_timeZoneName, 1) \ V(DateTime_timeZoneOffsetInSeconds, 1) \ V(DateTime_localTimeZoneAdjustmentInSeconds, 0) \ - V(AssertionError_throwNew, 2) \ + V(AssertionError_throwNew, 3) \ V(Async_rethrow, 2) \ V(StackTrace_current, 0) \ V(TypeError_throwNew, 5) \ diff --git a/runtime/vm/code_generator.cc b/runtime/vm/code_generator.cc index d0c07323864..495277caf59 100644 --- a/runtime/vm/code_generator.cc +++ b/runtime/vm/code_generator.cc @@ -585,7 +585,7 @@ DEFINE_RUNTIME_ENTRY(NonBoolTypeError, 1) { Instance::CheckedHandle(zone, arguments.ArgAt(0)); if (src_instance.IsNull()) { - const Array& args = Array::Handle(zone, Array::New(4)); + const Array& args = Array::Handle(zone, Array::New(5)); args.SetAt( 0, String::Handle( zone, @@ -596,6 +596,7 @@ DEFINE_RUNTIME_ENTRY(NonBoolTypeError, 1) { args.SetAt(1, String::Handle(zone, String::null())); args.SetAt(2, Smi::Handle(zone, Smi::New(0))); args.SetAt(3, Smi::Handle(zone, Smi::New(0))); + args.SetAt(4, String::Handle(zone, String::null())); Exceptions::ThrowByType(Exceptions::kAssertion, args); UNREACHABLE(); diff --git a/runtime/vm/parser.cc b/runtime/vm/parser.cc index 4ebafa283ba..16d8ec20652 100644 --- a/runtime/vm/parser.cc +++ b/runtime/vm/parser.cc @@ -70,6 +70,7 @@ DEFINE_FLAG(bool, assert_initializer, false, "Allow asserts in initializer lists."); +DEFINE_FLAG(bool, assert_message, false, "Allow message in assert statements"); DECLARE_FLAG(bool, profile_vm); DECLARE_FLAG(bool, trace_service); @@ -9164,32 +9165,91 @@ AstNode* Parser::ParseAssertStatement(bool is_const) { const TokenPosition condition_pos = TokenPos(); if (!I->asserts()) { SkipExpr(); + if (FLAG_assert_message && (CurrentToken() == Token::kCOMMA)) { + ConsumeToken(); + SkipExpr(); + } ExpectToken(Token::kRPAREN); return NULL; } - AstNode* condition = ParseAwaitableExpr(kAllowConst, kConsumeCascades, NULL); + + BoolScope saved_seen_await(&parsed_function()->have_seen_await_expr_, false); + AstNode* condition = ParseExpr(kAllowConst, kConsumeCascades); if (is_const && !condition->IsPotentiallyConst()) { ReportError(condition_pos, "initializer assert expression must be compile time constant."); } const TokenPosition condition_end = TokenPos(); + AstNode* message = NULL; + TokenPosition message_pos = TokenPosition::kNoSource; + if (FLAG_assert_message && CurrentToken() == Token::kCOMMA) { + ConsumeToken(); + message_pos = TokenPos(); + message = ParseExpr(kAllowConst, kConsumeCascades); + if (is_const && !message->IsPotentiallyConst()) { + ReportError( + message_pos, + "initializer assert expression must be compile time constant."); + } + } ExpectToken(Token::kRPAREN); + if (!is_const) { + // Check for assertion condition being a function if not const. + ArgumentListNode* arguments = new (Z) ArgumentListNode(condition_pos); + arguments->Add(condition); + condition = MakeStaticCall( + Symbols::AssertionError(), + Library::PrivateCoreLibName(Symbols::EvaluateAssertion()), arguments); + } + AstNode* not_condition = + new (Z) UnaryOpNode(condition_pos, Token::kNOT, condition); + + // Build call to _AsertionError._throwNew(start, end, message) ArgumentListNode* arguments = new (Z) ArgumentListNode(condition_pos); - arguments->Add(condition); arguments->Add(new (Z) LiteralNode( condition_pos, - Integer::ZoneHandle(Z, Integer::New(condition_pos.value(), Heap::kOld)))); + Integer::ZoneHandle(Z, Integer::New(condition_pos.Pos())))); arguments->Add(new (Z) LiteralNode( condition_end, - Integer::ZoneHandle(Z, Integer::New(condition_end.value(), Heap::kOld)))); + Integer::ZoneHandle(Z, Integer::New(condition_end.Pos())))); + if (message == NULL) { + message = new (Z) LiteralNode(condition_end, Instance::ZoneHandle(Z)); + } + arguments->Add(message); AstNode* assert_throw = MakeStaticCall( Symbols::AssertionError(), - Library::PrivateCoreLibName(is_const ? Symbols::CheckConstAssertion() - : Symbols::CheckAssertion()), - arguments); + Library::PrivateCoreLibName(Symbols::ThrowNew()), arguments); - return assert_throw; + AstNode* assertion_check = NULL; + if (parsed_function()->have_seen_await()) { + // The await transformation must be done manually because assertions + // are parsed as statements, not expressions. Thus, we need to check + // explicitely whether the arguments contain await operators. (Note that + // we must not parse the arguments with ParseAwaitableExpr(). In the + // corner case of assert(await a, await b), this would create two + // sibling scopes containing the temporary values for a and b. Both + // values would be allocated in the same internal context variable.) + // + // Build !condition ? _AsertionError._throwNew(...) : null; + // We need to use a conditional expression because the await transformer + // cannot transform if statements. + assertion_check = new (Z) ConditionalExprNode( + condition_pos, not_condition, assert_throw, + new (Z) LiteralNode(condition_pos, Object::null_instance())); + OpenBlock(); + AwaitTransformer at(current_block_->statements, async_temp_scope_); + AstNode* transformed_assertion = at.Transform(assertion_check); + SequenceNode* preamble = CloseBlock(); + preamble->Add(transformed_assertion); + assertion_check = preamble; + } else { + // Build if (!condition) _AsertionError._throwNew(...) + assertion_check = new (Z) + IfNode(condition_pos, not_condition, + NodeAsSequenceNode(condition_pos, assert_throw, NULL), NULL); + } + return assertion_check; } diff --git a/runtime/vm/symbols.h b/runtime/vm/symbols.h index 001810d5972..f1e1699094c 100644 --- a/runtime/vm/symbols.h +++ b/runtime/vm/symbols.h @@ -75,8 +75,7 @@ class ObjectPointerVisitor; V(_CompileTimeError, "_CompileTimeError") \ V(ThrowNew, "_throwNew") \ V(ThrowNewIfNotLoaded, "_throwNewIfNotLoaded") \ - V(CheckAssertion, "_checkAssertion") \ - V(CheckConstAssertion, "_checkConstAssertion") \ + V(EvaluateAssertion, "_evaluateAssertion") \ V(Symbol, "Symbol") \ V(SymbolCtor, "Symbol.") \ V(List, "List") \ diff --git a/sdk/lib/core/errors.dart b/sdk/lib/core/errors.dart index c11ea2e1a97..b6487a9dffe 100644 --- a/sdk/lib/core/errors.dart +++ b/sdk/lib/core/errors.dart @@ -96,7 +96,9 @@ class Error { * Error thrown by the runtime system when an assert statement fails. */ class AssertionError extends Error { - AssertionError(); + /** Message describing the assertion error. */ + final Object message; + AssertionError([this.message]); String toString() => "Assertion failed"; } diff --git a/tests/language/assert_message_test.dart b/tests/language/assert_message_test.dart new file mode 100644 index 00000000000..28499edb047 --- /dev/null +++ b/tests/language/assert_message_test.dart @@ -0,0 +1,104 @@ +// 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. + +// SharedOptions=--assert-message + +import "dart:async"; + +import "package:async_helper/async_helper.dart"; +import "package:expect/expect.dart"; + +main() { + // Only run with asserts enabled mode. + bool assertsEnabled = false; + assert(assertsEnabled = true); + if (!assertsEnabled) return; + + // Basics. + assert(true, ""); + assert(() => true, ""); + + int x = null; + // Successful asserts won't execute message. + assert(true, x + 42); + assert(true, throw "unreachable"); + + // Can use any value as message. + try { + assert(false, 42); + } on AssertionError catch (e) { + Expect.equals(42, e.message); + } + + try { + assert(false, ""); + } on AssertionError catch (e) { + Expect.equals("", e.message); + } + + try { + assert(false, null); + } on AssertionError catch (e) { + Expect.equals(null, e.message); + } + + // Test expression can throw. + try { + assert(throw "test", throw "message"); + } on String catch (e) { + Expect.equals("test", e); + } + + // Message expression can throw. + try { + assert(false, throw "message"); + } on String catch (e) { + Expect.equals("message", e); + } + + // Failing asserts evaluate message after test. + var list = []; + try { + assert((list..add(1)).isEmpty, (list..add(3)).length); + } on AssertionError catch (e) { + Expect.equals(2, e.message); + Expect.listEquals([1, 3], list); + } + + asyncStart(); + asyncTests().then((_) { asyncEnd(); }); +} + + +Future asyncTests() async { + // You can await in both condition and message. + assert(true, await 0); + assert(await true, 1); + assert(await true, await 2); + + // Successful asserts won't await/evaluate message. + void unreachable() => throw "unreachable"; + assert(await true, await unreachable()); + + try { + assert(false, await 3); + } on AssertionError catch (e) { + Expect.equals(3, e.message); + } + + var falseFuture = new Future.value(false); + var numFuture = new Future.value(4); + + try { + assert(await falseFuture, await numFuture); + } on AssertionError catch (e) { + Expect.equals(4, e.message); + } + + try { + assert(await falseFuture, await new Future.error("error")); + } on String catch (e) { + Expect.equals("error", e); + } +} diff --git a/tests/language/language_dart2js.status b/tests/language/language_dart2js.status index 0b2e449369e..03cfe396cd2 100644 --- a/tests/language/language_dart2js.status +++ b/tests/language/language_dart2js.status @@ -139,6 +139,7 @@ multiline_newline_test/06: MissingCompileTimeError # Issue 23888 multiline_newline_test/none: RuntimeError # Issue 23888 [ $compiler == dart2js && $checked ] +assert_message_test: Fail #28106 async_return_types_test/nestedFuture: Fail # Issue 26429 async_return_types_test/wrongTypeParameter: Fail # Issue 26429 regress_26133_test: RuntimeError # Issue 26429