From 2eba00fabbd4ef7972e352cf2cddf901ff94c6eb Mon Sep 17 00:00:00 2001 From: Chloe Stefantsova Date: Wed, 22 Mar 2023 17:21:45 +0000 Subject: [PATCH] [cfe] Split ensureAssignableResult into coersion and error reporting It allows to use coersion without error reporting in the inference of record literals, which makes the difference when the record patterns are involved. Closes https://github.com/dart-lang/sdk/issues/51587 Part of https://github.com/dart-lang/sdk/issues/49749 Change-Id: I64ea7e82eb3bc14d4410a47c6ed6ef278e092496 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/290529 Reviewed-by: Johnni Winther Commit-Queue: Chloe Stefantsova --- .../type_inference/inference_visitor.dart | 9 +- .../inference_visitor_base.dart | 165 ++++++++++++++++-- .../test/spell_checking_list_common.txt | 1 + .../testcases/patterns/issue51587.dart | 27 +++ .../patterns/issue51587.dart.strong.expect | 38 ++++ .../issue51587.dart.strong.transformed.expect | 45 +++++ .../issue51587.dart.textual_outline.expect | 3 + ...51587.dart.textual_outline_modelled.expect | 3 + .../patterns/issue51587.dart.weak.expect | 38 ++++ .../issue51587.dart.weak.modular.expect | 38 ++++ .../issue51587.dart.weak.outline.expect | 9 + .../issue51587.dart.weak.transformed.expect | 45 +++++ 12 files changed, 399 insertions(+), 22 deletions(-) create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.strong.expect create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.strong.transformed.expect create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.textual_outline.expect create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.textual_outline_modelled.expect create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.weak.expect create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.weak.modular.expect create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.weak.outline.expect create mode 100644 pkg/front_end/testcases/patterns/issue51587.dart.weak.transformed.expect diff --git a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart index 08d274d8eae..ac830b2bc33 100644 --- a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart +++ b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor.dart @@ -9146,7 +9146,8 @@ class InferenceVisitorImpl extends InferenceVisitorBase inferExpression(expression, contextType); if (contextType is! UnknownType) { expressionResult = - ensureAssignableResult(contextType, expressionResult); + coerceExpressionForAssignment(contextType, expressionResult) ?? + expressionResult; } positionalTypes.add( @@ -9194,7 +9195,8 @@ class InferenceVisitorImpl extends InferenceVisitorBase inferExpression(element.value, contextType); if (contextType is! UnknownType) { expressionResult = - ensureAssignableResult(contextType, expressionResult); + coerceExpressionForAssignment(contextType, expressionResult) ?? + expressionResult; } Expression expression = expressionResult.expression; DartType type = expressionResult.postCoercionType ?? @@ -9225,7 +9227,8 @@ class InferenceVisitorImpl extends InferenceVisitorBase inferExpression(element as Expression, contextType); if (contextType is! UnknownType) { expressionResult = - ensureAssignableResult(contextType, expressionResult); + coerceExpressionForAssignment(contextType, expressionResult) ?? + expressionResult; } Expression expression = expressionResult.expression; DartType type = expressionResult.postCoercionType ?? diff --git a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart index a455deed89c..1dcd086aea1 100644 --- a/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart +++ b/pkg/front_end/lib/src/fasta/type_inference/inference_visitor_base.dart @@ -445,16 +445,102 @@ abstract class InferenceVisitorBase implements InferenceVisitor { .expression; } - /// Same as [ensureAssignable], but accepts an [ExpressionInferenceResult] - /// rather than an expression and a type separately. If no change is made, - /// [inferenceResult] is returned unchanged. - ExpressionInferenceResult ensureAssignableResult( + /// Coerces expression ensuring its assignability to [contextType] + /// + /// If the expression is assignable without coercion, [inferenceResult] + /// is returned unchanged. If no coercion is possible for the given types, + /// `null` is returned. + ExpressionInferenceResult? coerceExpressionForAssignment( DartType contextType, ExpressionInferenceResult inferenceResult, {int? fileOffset, DartType? declaredContextType, DartType? runtimeCheckedType, bool isVoidAllowed = false, - bool coerceExpression = true, + bool coerceExpression = true}) { + // ignore: unnecessary_null_comparison + assert(contextType != null); + + fileOffset ??= inferenceResult.expression.fileOffset; + contextType = computeGreatestClosure(contextType); + + DartType initialContextType = runtimeCheckedType ?? contextType; + + Template? + preciseTypeErrorTemplate = + _getPreciseTypeErrorTemplate(inferenceResult.expression); + AssignabilityResult assignabilityResult = _computeAssignabilityKind( + contextType, inferenceResult.inferredType, + isNonNullableByDefault: isNonNullableByDefault, + isVoidAllowed: isVoidAllowed, + isExpressionTypePrecise: preciseTypeErrorTemplate != null, + coerceExpression: coerceExpression); + + if (assignabilityResult.needsTearOff) { + TypedTearoff typedTearoff = _tearOffCall(inferenceResult.expression, + inferenceResult.inferredType as InterfaceType, fileOffset); + inferenceResult = new ExpressionInferenceResult( + typedTearoff.tearoffType, typedTearoff.tearoff); + } + if (assignabilityResult.implicitInstantiation != null) { + inferenceResult = _applyImplicitInstantiation( + assignabilityResult.implicitInstantiation, + inferenceResult.inferredType, + inferenceResult.expression); + } + + DartType expressionType = inferenceResult.inferredType; + Expression expression = inferenceResult.expression; + switch (assignabilityResult.kind) { + case AssignabilityKind.assignable: + return inferenceResult; + case AssignabilityKind.assignableCast: + // Insert an implicit downcast. + Expression asExpression = + new AsExpression(expression, initialContextType) + ..isTypeError = true + ..isForNonNullableByDefault = isNonNullableByDefault + ..isForDynamic = expressionType is DynamicType + ..fileOffset = fileOffset; + flowAnalysis.forwardExpression(asExpression, expression); + return new ExpressionInferenceResult(expressionType, asExpression, + postCoercionType: initialContextType); + case AssignabilityKind.unassignable: + // Error: not assignable. Perform error recovery. + return null; + case AssignabilityKind.unassignableVoid: + // Error: not assignable. Perform error recovery. + return null; + case AssignabilityKind.unassignablePrecise: + // The type of the expression is known precisely, so an implicit + // downcast is guaranteed to fail. Insert a compile-time error. + return null; + case AssignabilityKind.unassignableCantTearoff: + return null; + case AssignabilityKind.unassignableNullability: + return null; + default: + return unhandled("${assignabilityResult}", "ensureAssignable", + fileOffset, helper.uri); + } + } + + /// Performs assignability checks on an expression + /// + /// [inferenceResult.expression] of type [inferenceResult.inferredType] is + /// checked for assignability to [contextType]. The errors are reported on the + /// current library and the expression wrapped in an [InvalidExpression], if + /// needed. If no change is made, [inferenceResult] is returned unchanged. + /// + /// If [isCoercionAllowed] is `true`, the assignability check is made + /// accounting for a possible coercion that may adjust the type of the + /// expression. + ExpressionInferenceResult reportAssignabilityErrors( + DartType contextType, ExpressionInferenceResult inferenceResult, + {int? fileOffset, + DartType? declaredContextType, + DartType? runtimeCheckedType, + bool isVoidAllowed = false, + bool isCoercionAllowed = true, Template? errorTemplate, Template? nullabilityErrorTemplate, @@ -492,8 +578,6 @@ abstract class InferenceVisitorBase implements InferenceVisitor { fileOffset ??= inferenceResult.expression.fileOffset; contextType = computeGreatestClosure(contextType); - DartType initialContextType = runtimeCheckedType ?? contextType; - Template? preciseTypeErrorTemplate = _getPreciseTypeErrorTemplate(inferenceResult.expression); @@ -502,7 +586,7 @@ abstract class InferenceVisitorBase implements InferenceVisitor { isNonNullableByDefault: isNonNullableByDefault, isVoidAllowed: isVoidAllowed, isExpressionTypePrecise: preciseTypeErrorTemplate != null, - coerceExpression: coerceExpression); + coerceExpression: isCoercionAllowed); if (assignabilityResult.needsTearOff) { TypedTearoff typedTearoff = _tearOffCall(inferenceResult.expression, @@ -519,20 +603,12 @@ abstract class InferenceVisitorBase implements InferenceVisitor { DartType expressionType = inferenceResult.inferredType; Expression expression = inferenceResult.expression; - Expression result; - DartType? postCoercionType; + DartType? postCoercionType = inferenceResult.postCoercionType; + Expression? result; switch (assignabilityResult.kind) { case AssignabilityKind.assignable: - result = expression; break; case AssignabilityKind.assignableCast: - // Insert an implicit downcast. - result = new AsExpression(expression, initialContextType) - ..isTypeError = true - ..isForNonNullableByDefault = isNonNullableByDefault - ..isForDynamic = expressionType is DynamicType - ..fileOffset = fileOffset; - postCoercionType = initialContextType; break; case AssignabilityKind.unassignable: // Error: not assignable. Perform error recovery. @@ -615,7 +691,7 @@ abstract class InferenceVisitorBase implements InferenceVisitor { fileOffset, helper.uri); } - if (!identical(result, expression)) { + if (result != null) { flowAnalysis.forwardExpression(result, expression); return new ExpressionInferenceResult(expressionType, result, postCoercionType: postCoercionType); @@ -624,6 +700,57 @@ abstract class InferenceVisitorBase implements InferenceVisitor { } } + /// Same as [ensureAssignable], but accepts an [ExpressionInferenceResult] + /// rather than an expression and a type separately. If no change is made, + /// [inferenceResult] is returned unchanged. + ExpressionInferenceResult ensureAssignableResult( + DartType contextType, ExpressionInferenceResult inferenceResult, + {int? fileOffset, + DartType? declaredContextType, + DartType? runtimeCheckedType, + bool isVoidAllowed = false, + bool coerceExpression = true, + Template? errorTemplate, + Template? + nullabilityErrorTemplate, + Template? nullabilityNullErrorTemplate, + Template? + nullabilityNullTypeErrorTemplate, + Template? + nullabilityPartErrorTemplate, + Map Function()? whyNotPromoted}) { + // ignore: unnecessary_null_comparison + assert(contextType != null); + + if (coerceExpression) { + ExpressionInferenceResult? coercionResult = coerceExpressionForAssignment( + contextType, inferenceResult, + fileOffset: fileOffset, + declaredContextType: declaredContextType, + runtimeCheckedType: runtimeCheckedType, + isVoidAllowed: isVoidAllowed, + coerceExpression: coerceExpression); + if (coercionResult != null) { + return coercionResult; + } + } + + inferenceResult = reportAssignabilityErrors(contextType, inferenceResult, + fileOffset: fileOffset, + declaredContextType: declaredContextType, + runtimeCheckedType: runtimeCheckedType, + isVoidAllowed: isVoidAllowed, + isCoercionAllowed: coerceExpression, + errorTemplate: errorTemplate, + nullabilityErrorTemplate: nullabilityErrorTemplate, + nullabilityNullErrorTemplate: nullabilityNullErrorTemplate, + nullabilityNullTypeErrorTemplate: nullabilityNullTypeErrorTemplate, + nullabilityPartErrorTemplate: nullabilityPartErrorTemplate, + whyNotPromoted: whyNotPromoted); + + return inferenceResult; + } + Expression _wrapTearoffErrorExpression(Expression expression, DartType contextType, Template template) { // ignore: unnecessary_null_comparison diff --git a/pkg/front_end/test/spell_checking_list_common.txt b/pkg/front_end/test/spell_checking_list_common.txt index 7face1cc5a2..c6b7fa40115 100644 --- a/pkg/front_end/test/spell_checking_list_common.txt +++ b/pkg/front_end/test/spell_checking_list_common.txt @@ -495,6 +495,7 @@ clue code coerce coerced +coerces coercing coercion coincides diff --git a/pkg/front_end/testcases/patterns/issue51587.dart b/pkg/front_end/testcases/patterns/issue51587.dart new file mode 100644 index 00000000000..407753fbdc4 --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart @@ -0,0 +1,27 @@ +// 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. + +test1() { + num b = 0; + b as int; + b.isEven; + (b,) = (3.14,); + return b; +} + +test2() { + num b = 0; + b as int; + b.isEven; + (b, foo: _) = (3.14, foo: "foo"); + return b; +} + +test3() { + num b = 0; + b as int; + b.isEven; + (foo: b) = (foo: 3.14); + return b; +} diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.strong.expect b/pkg/front_end/testcases/patterns/issue51587.dart.strong.expect new file mode 100644 index 00000000000..b7e9a8a8327 --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.strong.expect @@ -0,0 +1,38 @@ +library /*isNonNullableByDefault*/; +import self as self; +import "dart:core" as core; +import "dart:_internal" as _in; + +static method test1() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14); + if(!(let final dynamic #t1 = b = #0#0{(core::double)}.$1{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test2() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14, {foo: "foo"}); + if(!((let final dynamic #t2 = b = #0#0{(core::double, {foo: core::String})}.$1{core::double} in true) && true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test3() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = ({foo: 3.14}); + if(!(let final dynamic #t3 = b = #0#0{({foo: core::double})}.foo{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.strong.transformed.expect b/pkg/front_end/testcases/patterns/issue51587.dart.strong.transformed.expect new file mode 100644 index 00000000000..f4ef8a4e8df --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.strong.transformed.expect @@ -0,0 +1,45 @@ +library /*isNonNullableByDefault*/; +import self as self; +import "dart:core" as core; +import "dart:_internal" as _in; + +static method test1() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14); + if(!(let final core::double #t1 = b = #0#0{(core::double)}.$1{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test2() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14, {foo: "foo"}); + if(!((let final core::double #t2 = b = #0#0{(core::double, {foo: core::String})}.$1{core::double} in true) && true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test3() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = ({foo: 3.14}); + if(!(let final core::double #t3 = b = #0#0{({foo: core::double})}.foo{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} + + +Extra constant evaluation status: +Evaluated: RecordLiteral @ org-dartlang-testcase:///issue51587.dart:9:10 -> RecordConstant(const (3.14)) +Evaluated: RecordLiteral @ org-dartlang-testcase:///issue51587.dart:17:17 -> RecordConstant(const (3.14, {foo: "foo"})) +Evaluated: RecordLiteral @ org-dartlang-testcase:///issue51587.dart:25:14 -> RecordConstant(const ({foo: 3.14})) +Extra constant evaluation: evaluated: 46, effectively constant: 3 diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.textual_outline.expect b/pkg/front_end/testcases/patterns/issue51587.dart.textual_outline.expect new file mode 100644 index 00000000000..3615dc47ce6 --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.textual_outline.expect @@ -0,0 +1,3 @@ +test1() {} +test2() {} +test3() {} diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.textual_outline_modelled.expect b/pkg/front_end/testcases/patterns/issue51587.dart.textual_outline_modelled.expect new file mode 100644 index 00000000000..3615dc47ce6 --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.textual_outline_modelled.expect @@ -0,0 +1,3 @@ +test1() {} +test2() {} +test3() {} diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.weak.expect b/pkg/front_end/testcases/patterns/issue51587.dart.weak.expect new file mode 100644 index 00000000000..b7e9a8a8327 --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.weak.expect @@ -0,0 +1,38 @@ +library /*isNonNullableByDefault*/; +import self as self; +import "dart:core" as core; +import "dart:_internal" as _in; + +static method test1() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14); + if(!(let final dynamic #t1 = b = #0#0{(core::double)}.$1{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test2() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14, {foo: "foo"}); + if(!((let final dynamic #t2 = b = #0#0{(core::double, {foo: core::String})}.$1{core::double} in true) && true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test3() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = ({foo: 3.14}); + if(!(let final dynamic #t3 = b = #0#0{({foo: core::double})}.foo{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.weak.modular.expect b/pkg/front_end/testcases/patterns/issue51587.dart.weak.modular.expect new file mode 100644 index 00000000000..b7e9a8a8327 --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.weak.modular.expect @@ -0,0 +1,38 @@ +library /*isNonNullableByDefault*/; +import self as self; +import "dart:core" as core; +import "dart:_internal" as _in; + +static method test1() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14); + if(!(let final dynamic #t1 = b = #0#0{(core::double)}.$1{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test2() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14, {foo: "foo"}); + if(!((let final dynamic #t2 = b = #0#0{(core::double, {foo: core::String})}.$1{core::double} in true) && true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test3() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = ({foo: 3.14}); + if(!(let final dynamic #t3 = b = #0#0{({foo: core::double})}.foo{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.weak.outline.expect b/pkg/front_end/testcases/patterns/issue51587.dart.weak.outline.expect new file mode 100644 index 00000000000..958a7a5e8fe --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.weak.outline.expect @@ -0,0 +1,9 @@ +library /*isNonNullableByDefault*/; +import self as self; + +static method test1() → dynamic + ; +static method test2() → dynamic + ; +static method test3() → dynamic + ; diff --git a/pkg/front_end/testcases/patterns/issue51587.dart.weak.transformed.expect b/pkg/front_end/testcases/patterns/issue51587.dart.weak.transformed.expect new file mode 100644 index 00000000000..f4ef8a4e8df --- /dev/null +++ b/pkg/front_end/testcases/patterns/issue51587.dart.weak.transformed.expect @@ -0,0 +1,45 @@ +library /*isNonNullableByDefault*/; +import self as self; +import "dart:core" as core; +import "dart:_internal" as _in; + +static method test1() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14); + if(!(let final core::double #t1 = b = #0#0{(core::double)}.$1{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test2() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = (3.14, {foo: "foo"}); + if(!((let final core::double #t2 = b = #0#0{(core::double, {foo: core::String})}.$1{core::double} in true) && true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} +static method test3() → dynamic { + core::num b = 0; + b as{ForNonNullableByDefault} core::int; + b{core::int}.{core::int::isEven}{core::bool}; + block { + final synthesized dynamic #0#0 = ({foo: 3.14}); + if(!(let final core::double #t3 = b = #0#0{({foo: core::double})}.foo{core::double} in true)) + throw new _in::ReachabilityError::•(); + } =>#0#0; + return b; +} + + +Extra constant evaluation status: +Evaluated: RecordLiteral @ org-dartlang-testcase:///issue51587.dart:9:10 -> RecordConstant(const (3.14)) +Evaluated: RecordLiteral @ org-dartlang-testcase:///issue51587.dart:17:17 -> RecordConstant(const (3.14, {foo: "foo"})) +Evaluated: RecordLiteral @ org-dartlang-testcase:///issue51587.dart:25:14 -> RecordConstant(const ({foo: 3.14})) +Extra constant evaluation: evaluated: 46, effectively constant: 3