[dart2js] Annotation for exception stacks with shorter prefix
Dart's `throw` expression does not correspond exactly to JavaScript's `throw` statement. To implement Dart semantics, and to reduce code size, dart2js uses the functions `wrapException` and `throwExpression` from the dart2js runtime. As a result, these functions appear in the stack trace (one or both).
A consequence of this is that a fixed prefix of an error stack is less useful, as potentially interesting frames are forced out of the prefix by these 'noise' frames.
This change ensures only one of the helpers on the stack trace.
It is also possible to get rid of the helper frame by an annotation.
The annotation `@pragma('dart2js:stack-starts-at-throw')` causes the stack to be captured in the current JavaScript function rather than in a helper. The cost is more code at the call site, about 12 bytes per `throw` expression in minified code. The annotation can be placed on a method, class or library, and applies to all `throw` expression in the scope of the annotated element.
This change uses the annotation to remove noise frames from type errors and some other errors in the runtime.
Change-Id: If15184a5963fb054199177bb4526b32f25e53fe9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/406684
Reviewed-by: Mayank Patke <fishythefish@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
diff --git a/pkg/compiler/doc/pragmas.md b/pkg/compiler/doc/pragmas.md
index c789e04..d3261f3 100644
--- a/pkg/compiler/doc/pragmas.md
+++ b/pkg/compiler/doc/pragmas.md
@@ -12,6 +12,7 @@
| `dart2js:noElision` | Disables an optimization whereby unused fields or unused parameters are removed |
| `dart2js:load-priority` | [Affects deferred library loading](#load-priority) |
| `dart2js:resource-identifier` | [Collects data references to resources](resource_identifiers.md) |
+| `dart2js:stack-starts-at-throw` | [Affects stack trace from `throw` expressions](#stack-statrs-at-throw) |
| `weak-tearoff-reference` | [Declaring a static weak reference intrinsic method.](#declaring-a-static-weak-reference-intrinsic-method) |
## Unsafe pragmas for general use
@@ -214,6 +215,21 @@
variables, static variables, and top-level variables.
+#### Stack starts at throw
+
+In order to generate smaller code, `throw` expressions are usually implemented
+by calling a helper method to capture the stack trace and perform the actual
+throw. This adds one (or occasionally two) frames to the stack trace showing the
+helper functions. It is possible for the stack trace to be captured directly in
+the method containing the `throw` expression. This takes a little extra code, so
+is opt-in via the following annotation.
+
+```dart
+@pragma('dart2js:stack-starts-at-throw')
+```
+
+This annotation can be placed on a method, class or library.
+
### Annotations related to deferred library loading
#### Load priority
diff --git a/pkg/compiler/lib/src/common/elements.dart b/pkg/compiler/lib/src/common/elements.dart
index 4984098..b2c9f0b 100644
--- a/pkg/compiler/lib/src/common/elements.dart
+++ b/pkg/compiler/lib/src/common/elements.dart
@@ -898,6 +898,9 @@
FunctionEntity get stringInterpolationHelper => _findHelperFunction('S');
+ FunctionEntity get initializeExceptionWrapper =>
+ _findHelperFunction('initializeExceptionWrapper');
+
FunctionEntity get wrapExceptionHelper =>
_findHelperFunction('wrapException');
diff --git a/pkg/compiler/lib/src/js_backend/annotations.dart b/pkg/compiler/lib/src/js_backend/annotations.dart
index fcd30ce..1719160 100644
--- a/pkg/compiler/lib/src/js_backend/annotations.dart
+++ b/pkg/compiler/lib/src/js_backend/annotations.dart
@@ -91,7 +91,9 @@
lateCheck('late:check'),
loadLibraryPriority('load-priority', hasOption: true),
- resourceIdentifier('resource-identifier');
+ resourceIdentifier('resource-identifier'),
+
+ throwWithoutHelperFrame('stack-starts-at-throw');
final String name;
final bool forFunctionsOnly;
@@ -357,6 +359,11 @@
/// Determines whether [member] is annotated as a resource identifier.
bool methodIsResourceIdentifier(FunctionEntity member);
+
+ /// Is this node in a context requesting that the captured stack in a `throw`
+ /// expression generates extra code to avoid having a runtime helper on the
+ /// stack?
+ bool throwWithoutHelperFrame(ir.TreeNode node);
}
class AnnotationsDataImpl implements AnnotationsData {
@@ -645,6 +652,22 @@
}
return false;
}
+
+ @override
+ bool throwWithoutHelperFrame(ir.TreeNode node) {
+ return _throwWithoutHelperFrame(_findContext(node));
+ }
+
+ bool _throwWithoutHelperFrame(DirectivesContext? context) {
+ while (context != null) {
+ EnumSet<PragmaAnnotation>? annotations = context.annotations;
+ if (annotations.contains(PragmaAnnotation.throwWithoutHelperFrame)) {
+ return true;
+ }
+ context = context.parent;
+ }
+ return false;
+ }
}
class AnnotationsDataBuilder {
diff --git a/pkg/compiler/lib/src/ssa/builder.dart b/pkg/compiler/lib/src/ssa/builder.dart
index bda4f1d..183b61b 100644
--- a/pkg/compiler/lib/src/ssa/builder.dart
+++ b/pkg/compiler/lib/src/ssa/builder.dart
@@ -2529,7 +2529,14 @@
final sourceInformation = _sourceInformationBuilder.buildThrow(
node.expression,
);
- _closeAndGotoExit(HThrow(pop(), sourceInformation));
+ _closeAndGotoExit(
+ HThrow(
+ pop(),
+ sourceInformation,
+ withoutHelperFrame: closedWorld.annotationsData
+ .throwWithoutHelperFrame(node),
+ ),
+ );
} else {
expression.accept(this);
pop();
@@ -7759,6 +7766,8 @@
pop(),
_abstractValueDomain.emptyType,
sourceInformation,
+ withoutHelperFrame: closedWorld.annotationsData
+ .throwWithoutHelperFrame(node),
),
);
_isReachable = false;
diff --git a/pkg/compiler/lib/src/ssa/codegen.dart b/pkg/compiler/lib/src/ssa/codegen.dart
index 0929e05..8341e1f 100644
--- a/pkg/compiler/lib/src/ssa/codegen.dart
+++ b/pkg/compiler/lib/src/ssa/codegen.dart
@@ -2994,13 +2994,24 @@
pushStatement(js.Throw(pop()).withSourceInformation(sourceInformation));
} else {
use(node.inputs[0]);
- _pushCallStatic(_commonElements.wrapExceptionHelper, [
- pop(),
- ], sourceInformation);
+ if (node.withoutHelperFrame) {
+ _pushCallStatic(_commonElements.initializeExceptionWrapper, [
+ pop(),
+ _newErrorObject(sourceInformation),
+ ], sourceInformation);
+ } else {
+ _pushCallStatic(_commonElements.wrapExceptionHelper, [
+ pop(),
+ ], sourceInformation);
+ }
pushStatement(js.Throw(pop()).withSourceInformation(sourceInformation));
}
}
+ js.Expression _newErrorObject(SourceInformation? sourceInformation) {
+ return js.js('new Error()').withSourceInformation(sourceInformation);
+ }
+
@override
void visitAwait(HAwait node) {
use(node.inputs[0]);
@@ -3185,6 +3196,7 @@
use(node.inputs[0]);
_pushCallStatic(_commonElements.throwExpressionHelper, [
pop(),
+ if (node.withoutHelperFrame) _newErrorObject(node.sourceInformation),
], node.sourceInformation);
}
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index 342b756..d9ebfa9 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -3417,11 +3417,13 @@
}
class HThrowExpression extends HInstruction {
+ final bool withoutHelperFrame;
HThrowExpression(
super.value,
super.type,
- SourceInformation? sourceInformation,
- ) : super._oneInput() {
+ SourceInformation? sourceInformation, {
+ this.withoutHelperFrame = false,
+ }) : super._oneInput() {
this.sourceInformation = sourceInformation;
}
@override
@@ -3466,10 +3468,12 @@
class HThrow extends HControlFlow {
final bool isRethrow;
+ final bool withoutHelperFrame;
HThrow(
HInstruction value,
SourceInformation? sourceInformation, {
this.isRethrow = false,
+ this.withoutHelperFrame = false,
}) {
inputs.add(value);
this.sourceInformation = sourceInformation;
diff --git a/pkg/compiler/test/impact/data/expressions.dart b/pkg/compiler/test/impact/data/expressions.dart
index 2ec5a828..80dba1d 100644
--- a/pkg/compiler/test/impact/data/expressions.dart
+++ b/pkg/compiler/test/impact/data/expressions.dart
@@ -358,8 +358,11 @@
testAsGenericDynamic(dynamic o) => o as GenericClass<dynamic, dynamic>;
/*member: testThrow:
- static=[throwExpression(1),wrapException(1)],
- type=[inst:JSString]*/
+ static=[
+ throwExpression(2),
+ wrapException(1)],
+ type=[inst:JSString]
+*/
testThrow() => throw '';
/*member: testIfNotNull:
diff --git a/pkg/compiler/test/impact/data/statements.dart b/pkg/compiler/test/impact/data/statements.dart
index fecc513..982097b 100644
--- a/pkg/compiler/test/impact/data/statements.dart
+++ b/pkg/compiler/test/impact/data/statements.dart
@@ -424,7 +424,7 @@
/*member: testSwitchWithoutFallthrough:
static=[
- throwExpression(1),
+ throwExpression(2),
wrapException(1)],
type=[
inst:JSInt,
diff --git a/pkg/compiler/test/sourcemaps/stacktrace_test.dart b/pkg/compiler/test/sourcemaps/stacktrace_test.dart
index 640a7a5..aac20dc 100644
--- a/pkg/compiler/test/sourcemaps/stacktrace_test.dart
+++ b/pkg/compiler/test/sourcemaps/stacktrace_test.dart
@@ -124,6 +124,7 @@
const List<LineException> beforeExceptions = const [
const LineException('wrapException', 'js_helper.dart'),
const LineException('throwExpression', 'js_helper.dart'),
+ const LineException('throw_', 'js_helper.dart'),
];
/// Lines allowed after the intended stack trace. Typically from the event
diff --git a/sdk/lib/_internal/js_runtime/lib/async_patch.dart b/sdk/lib/_internal/js_runtime/lib/async_patch.dart
index 3bca5bf..ec48de1 100644
--- a/sdk/lib/_internal/js_runtime/lib/async_patch.dart
+++ b/sdk/lib/_internal/js_runtime/lib/async_patch.dart
@@ -12,7 +12,6 @@
getTraceFromException,
Primitives,
requiresPreamble,
- wrapException,
unwrapException;
import 'dart:_foreign_helper'
diff --git a/sdk/lib/_internal/js_runtime/lib/core_patch.dart b/sdk/lib/_internal/js_runtime/lib/core_patch.dart
index 156b4be..a031b4a 100644
--- a/sdk/lib/_internal/js_runtime/lib/core_patch.dart
+++ b/sdk/lib/_internal/js_runtime/lib/core_patch.dart
@@ -23,7 +23,7 @@
quoteStringForRegExp,
getTraceFromException,
RuntimeError,
- wrapException,
+ initializeExceptionWrapper,
wrapZoneUnaryCallback,
TrustedGetRuntimeType;
@@ -273,11 +273,11 @@
StackTrace? get stackTrace => Primitives.extractStackTrace(this);
@patch
+ @pragma('dart2js:never-inline')
static Never _throw(Object error, StackTrace stackTrace) {
- error = wrapException(error);
+ error = initializeExceptionWrapper(error, JS('', 'new Error()'));
JS('void', '#.stack = #', error, stackTrace.toString());
- JS('', 'throw #', error);
- throw "unreachable";
+ JS<Never>('', 'throw #', error);
}
}
diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
index cc4d44e..5404cfe 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
@@ -1100,7 +1100,8 @@
static void trySetStackTrace(Error error, StackTrace stackTrace) {
var jsError = JS('', r'#.$thrownJsError', error);
if (jsError == null) {
- jsError = wrapException(error);
+ jsError = JS('', 'new Error()');
+ initializeExceptionWrapper(error, jsError);
JS('', r'#.$thrownJsError = #', error, jsError);
JS('void', '#.stack = #', jsError, stackTrace.toString());
}
@@ -1200,18 +1201,23 @@
return value;
}
-/// Wrap the given Dart object as a JS `Error` that can carry a stack trace.
+/// Wrap the given Dart object as a JavaScript `Error` that can carry a stack
+/// trace.
///
/// The code in [unwrapException] deals with getting the original Dart
/// object out of the wrapper again.
@pragma('dart2js:never-inline')
wrapException(ex) {
final wrapper = JS('', 'new Error()');
- return initializeExceptionWrapper(wrapper, ex);
+ return initializeExceptionWrapper(ex, wrapper);
}
+/// Wrap the given Dart object with the recorded stack trace.
+///
+/// The code in [unwrapException] deals with getting the original Dart
+/// object out of the wrapper again. For convenience, returns [wrapper].
@pragma('dart2js:never-inline')
-initializeExceptionWrapper(wrapper, ex) {
+initializeExceptionWrapper(ex, wrapper) {
if (ex == null) ex = TypeError();
// [unwrapException] looks for the property 'dartException'.
JS('void', '#.dartException = #', wrapper, ex);
@@ -1240,18 +1246,14 @@
return JS('', r'this.dartException').toString();
}
-/// This wraps the exception and does the throw. It is possible to call this in
-/// a JS expression context, where the throw statement is not allowed. Helpers
-/// are never inlined, so we don't risk inlining the throw statement into an
-/// expression context.
+/// This wraps the exception and does the throw. Since this is a function, it
+/// is possible to call this in a JavaScript expression context, where the throw
+/// statement is not allowed. The wrapper may be passed in, but usually
+/// [throwExpression] is called with one argument, leaving `wrapper` undefined.
@pragma('dart2js:never-inline')
-Never throwExpression(ex) {
- // TODO(sra): Manually inline `wrapException` to remove one stack frame.
- JS<Never>('', 'throw #', wrapException(ex));
-}
-
-Never throwExpressionWithWrapper(ex, wrapper) {
- JS<Never>('', 'throw #', initializeExceptionWrapper(wrapper, ex));
+Never throwExpression(ex, [wrapper]) {
+ wrapper ??= JS('', 'new Error()');
+ JS<Never>('', 'throw #', initializeExceptionWrapper(ex, wrapper));
}
throwUnsupportedError(message) {
@@ -1272,8 +1274,7 @@
operation ??= 0;
verb ??= 0;
final wrapper = JS('', 'Error()');
- throwExpressionWithWrapper(
- _diagnoseUnsupportedOperation(o, operation, verb), wrapper);
+ throwExpression(_diagnoseUnsupportedOperation(o, operation, verb), wrapper);
}
@pragma('dart2js:never-inline')
diff --git a/sdk/lib/_internal/js_runtime/lib/late_helper.dart b/sdk/lib/_internal/js_runtime/lib/late_helper.dart
index 30c59d4..81f9ade 100644
--- a/sdk/lib/_internal/js_runtime/lib/late_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/late_helper.dart
@@ -5,43 +5,42 @@
library _late_helper;
import 'dart:_internal' show LateError, createSentinel, isSentinel;
-import 'dart:_js_helper' show throwExpressionWithWrapper;
import 'dart:_foreign_helper' show JS;
@pragma('dart2js:never-inline')
+@pragma('dart2js:stack-starts-at-throw')
void throwLateFieldNI(String fieldName) {
- final wrapper = JS('', 'new Error()');
- throwExpressionWithWrapper(LateError.fieldNI(fieldName), wrapper);
+ throw LateError.fieldNI(fieldName);
}
@pragma('dart2js:never-inline')
+@pragma('dart2js:stack-starts-at-throw')
void throwLateFieldAI(String fieldName) {
- final wrapper = JS('', 'new Error()');
- throwExpressionWithWrapper(LateError.fieldAI(fieldName), wrapper);
+ throw LateError.fieldAI(fieldName);
}
@pragma('dart2js:never-inline')
+@pragma('dart2js:stack-starts-at-throw')
void throwLateFieldADI(String fieldName) {
- final wrapper = JS('', 'new Error()');
- throwExpressionWithWrapper(LateError.fieldADI(fieldName), wrapper);
+ throw LateError.fieldADI(fieldName);
}
@pragma('dart2js:never-inline')
+@pragma('dart2js:stack-starts-at-throw')
void throwUnnamedLateFieldNI() {
- final wrapper = JS('', 'new Error()');
- throwExpressionWithWrapper(LateError.fieldNI(''), wrapper);
+ throw LateError.fieldNI('');
}
@pragma('dart2js:never-inline')
+@pragma('dart2js:stack-starts-at-throw')
void throwUnnamedLateFieldAI() {
- final wrapper = JS('', 'new Error()');
- throwExpressionWithWrapper(LateError.fieldAI(''), wrapper);
+ throw LateError.fieldAI('');
}
@pragma('dart2js:never-inline')
+@pragma('dart2js:stack-starts-at-throw')
void throwUnnamedLateFieldADI() {
- final wrapper = JS('', 'new Error()');
- throwExpressionWithWrapper(LateError.fieldADI(''), wrapper);
+ throw LateError.fieldADI('');
}
/// A boxed variable used for lowering uninitialized `late` variables when they
diff --git a/sdk/lib/_internal/js_shared/lib/rti.dart b/sdk/lib/_internal/js_shared/lib/rti.dart
index 44841dd..82e798f 100644
--- a/sdk/lib/_internal/js_shared/lib/rti.dart
+++ b/sdk/lib/_internal/js_shared/lib/rti.dart
@@ -1531,6 +1531,7 @@
/// General unspecialized 'as' check that works for any type.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
Object? _generalAsCheckImplementation(Object? object) {
// This static method is installed on an Rti object as a JavaScript instance
// method. The Rti object is 'this'.
@@ -1543,7 +1544,7 @@
if (JS_GET_FLAG('LEGACY')) {
if (JS_GET_FLAG('EXTRA_NULL_SAFETY_CHECKS')) {
_onExtraNullSafetyError(
- _failedAsCheckError(object, testRti),
+ _errorForAsCheck(object, testRti),
StackTrace.current,
);
}
@@ -1551,11 +1552,12 @@
}
} else if (Rti._isCheck(testRti, object))
return object;
- _failedAsCheck(object, testRti);
+ throw _errorForAsCheck(object, testRti);
}
/// General 'as' check for types that accept `null`.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
Object? _generalNullableAsCheckImplementation(Object? object) {
// This static method is installed on an Rti object as a JavaScript instance
// method. The Rti object is 'this'.
@@ -1564,20 +1566,16 @@
return object;
} else if (Rti._isCheck(testRti, object))
return object;
- _failedAsCheck(object, testRti);
+ throw _errorForAsCheck(object, testRti);
}
-_TypeError _failedAsCheckError(Object? object, Rti testRti) {
+_TypeError _errorForAsCheck(Object? object, Rti testRti) {
String message = _Error.compose(object, _rtiToString(testRti, null));
return _TypeError.fromMessage(message);
}
-@pragma('dart2js:prefer-inline')
-Never _failedAsCheck(Object? object, Rti testRti) {
- throw _failedAsCheckError(object, testRti);
-}
-
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
Rti checkTypeBound(Rti type, Rti bound, String variable, String methodName) {
if (isSubtype(_theUniverse(), type, bound)) return type;
String message =
@@ -1588,6 +1586,7 @@
}
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
Never throwTypeError(String message) {
throw _TypeError.fromMessage(message);
}
@@ -1646,6 +1645,7 @@
/// Specialization for 'as Object'.
/// Called from generated code via Rti `_as` method.
+@pragma('dart2js:stack-starts-at-throw')
Object? _asObject(Object? object) {
if (object != null) return object;
if (JS_GET_FLAG('LEGACY')) {
@@ -1688,6 +1688,7 @@
/// Specialization for 'as bool'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
bool _asBool(Object? object) {
if (true == object) return true;
if (false == object) return false;
@@ -1696,6 +1697,7 @@
/// Specialization for 'as bool*'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
bool? _asBoolS(dynamic object) {
if (true == object) return true;
if (false == object) return false;
@@ -1713,6 +1715,7 @@
/// Specialization for 'as bool?'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
bool? _asBoolQ(dynamic object) {
if (true == object) return true;
if (false == object) return false;
@@ -1722,6 +1725,7 @@
/// Specialization for 'as double'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
double _asDouble(Object? object) {
if (_isNum(object)) return _Utils.asDouble(object);
throw _TypeError.forType(object, 'double');
@@ -1729,6 +1733,7 @@
/// Specialization for 'as double*'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
double? _asDoubleS(dynamic object) {
if (_isNum(object)) return _Utils.asDouble(object);
if (object == null) {
@@ -1745,6 +1750,7 @@
/// Specialization for 'as double?'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
double? _asDoubleQ(dynamic object) {
if (_isNum(object)) return _Utils.asDouble(object);
if (object == null) return _Utils.asNull(object);
@@ -1760,6 +1766,7 @@
/// Specialization for 'as int'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
int _asInt(Object? object) {
if (_isInt(object)) return _Utils.asInt(object);
throw _TypeError.forType(object, 'int');
@@ -1767,6 +1774,7 @@
/// Specialization for 'as int*'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
int? _asIntS(dynamic object) {
if (_isInt(object)) return _Utils.asInt(object);
if (object == null) {
@@ -1783,6 +1791,7 @@
/// Specialization for 'as int?'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
int? _asIntQ(dynamic object) {
if (_isInt(object)) return _Utils.asInt(object);
if (object == null) return _Utils.asNull(object);
@@ -1797,6 +1806,7 @@
/// Specialization for 'as num'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
num _asNum(Object? object) {
if (_isNum(object)) return _Utils.asNum(object);
throw _TypeError.forType(object, 'num');
@@ -1804,6 +1814,7 @@
/// Specialization for 'as num*'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
num? _asNumS(dynamic object) {
if (_isNum(object)) return _Utils.asNum(object);
if (object == null) {
@@ -1820,6 +1831,7 @@
/// Specialization for 'as num?'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
num? _asNumQ(dynamic object) {
if (_isNum(object)) return _Utils.asNum(object);
if (object == null) return _Utils.asNull(object);
@@ -1834,6 +1846,7 @@
/// Specialization for 'as String'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
String _asString(Object? object) {
if (_isString(object)) return _Utils.asString(object);
throw _TypeError.forType(object, 'String');
@@ -1841,6 +1854,7 @@
/// Specialization for 'as String*'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
String? _asStringS(dynamic object) {
if (_isString(object)) return _Utils.asString(object);
if (object == null) {
@@ -1857,6 +1871,7 @@
/// Specialization for 'as String?'.
/// Called from generated code.
+@pragma('dart2js:stack-starts-at-throw')
String? _asStringQ(dynamic object) {
if (_isString(object)) return _Utils.asString(object);
if (object == null) return _Utils.asNull(object);