Skip to content

Commit

Permalink
[pkg/vm] Enable const functions during platform const field evaluation.
Browse files Browse the repository at this point in the history
This allows more complicated field initializers that are written using
immediately invoked closures, like

@pragma("vm:platform-const")
final bool foo = () {
  ... do some stuff ...
}();

to be properly evaluated by the VM constant evaluator.

Also throws errors at compile time if the annotated member cannot be
evaluated to a constant or if an invalid member (not an
initialized static field or a static getter) is annotated.

TEST=pkg/vm/test/transformations/vm_constant_evaluator_test

Issue: #31969
Issue: #50473
Issue: flutter/flutter#14233
Change-Id: I14be498bb5f7771f0f339baf7d3b1bec7df5903f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/348380
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
  • Loading branch information
sstrickl authored and Commit Queue committed Jan 30, 2024
1 parent f992bfc commit 0eac43b
Show file tree
Hide file tree
Showing 47 changed files with 876 additions and 503 deletions.
2 changes: 1 addition & 1 deletion pkg/front_end/lib/src/fasta/kernel/constant_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2451,7 +2451,7 @@ class ConstantEvaluator implements ExpressionVisitor<Constant> {

final bool enableTripleShift;
final bool enableAsserts;
bool enableConstFunctions;
final bool enableConstFunctions;
bool inExtensionTypeConstConstructor = false;

final Map<Constant, Constant> canonicalizationCache;
Expand Down
74 changes: 65 additions & 9 deletions pkg/vm/lib/transformations/unreachable_code_elimination.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,56 @@ class SimpleUnreachableCodeElimination extends RemovingTransformer {
SimpleUnreachableCodeElimination(this.constantEvaluator,
{required this.enableAsserts, required this.soundNullSafety});

bool _shouldEvaluateMember(Member node) =>
constantEvaluator.hasTargetOS && constantEvaluator.isPlatformConst(node);

Never _throwPlatformConstError(Member node, String message) {
final uri = constantEvaluator.getFileUri(node);
final offset = constantEvaluator.getFileOffset(uri, node);
throw PlatformConstError(message, node, uri, offset);
}

void _checkPlatformConstMember(Member node) {
if (node is Field) {
if (!node.isStatic) {
_throwPlatformConstError(node, 'not a static field');
}
// Static fields currently always have an initializer set, even if it's
// the implicit null initializer for a nullable field.
assert(node.initializer != null);
} else if (node is Procedure) {
if (!node.isStatic) {
_throwPlatformConstError(node, 'not a static method');
}
if (!node.isGetter) {
_throwPlatformConstError(node, 'not a getter');
}
} else {
_throwPlatformConstError(node, 'not a field or method');
}
}

@override
TreeNode defaultMember(Member node, TreeNode? removalSentinel) {
_staticTypeContext =
StaticTypeContext(node, constantEvaluator.typeEnvironment);
if (_shouldEvaluateMember(node)) {
_checkPlatformConstMember(node);
// Create a StaticGet to ensure the member is evaluated at least once,
// and then replace the field initializer or getter body with the result.
final staticGet = StaticGet(node)..fileOffset = node.fileOffset;
final result =
staticGet.accept1(this, cannotRemoveSentinel) as ConstantExpression;
if (node is Field) {
node.initializer = result
..fileOffset = node.initializer!.fileOffset
..parent = node;
} else if (node is Procedure) {
node.function.body = ReturnStatement(result)
..fileOffset = node.function.body!.fileOffset
..parent = node.function;
}
}
final result = super.defaultMember(node, removalSentinel);
_staticTypeContext = null;
return result;
Expand Down Expand Up @@ -205,20 +251,17 @@ class SimpleUnreachableCodeElimination extends RemovingTransformer {
throw 'StaticGet from const field $target should be evaluated by front-end: $node';
}

if (!constantEvaluator.hasTargetOS ||
!constantEvaluator.isPlatformConst(target)) {
if (!_shouldEvaluateMember(target)) {
return super.visitStaticGet(node, removalSentinel);
}

final result = constantEvaluator.evaluate(_staticTypeContext!, node);

if (result is UnevaluatedConstant &&
result.expression is InvalidExpression) {
return result.expression;
_checkPlatformConstMember(target);
final constant = constantEvaluator.evaluate(_staticTypeContext!, node);
if (constant is UnevaluatedConstant) {
_throwPlatformConstError(target, 'cannot evaluate to a constant');
}

final type = node.getStaticType(_staticTypeContext!);
return ConstantExpression(result, type)..fileOffset = node.fileOffset;
return ConstantExpression(constant, type)..fileOffset = node.fileOffset;
}

@override
Expand Down Expand Up @@ -335,3 +378,16 @@ class ContinueSwitchStatementTargetCollector extends RecursiveVisitor {
}
}
}

class PlatformConstError extends Error {
final Object? message;
final Member member;
final Uri? uri;
final int offset;

PlatformConstError(this.message, this.member, this.uri, this.offset);

@override
String toString() => '${uri ?? ''}:$offset '
'Error for annotated member ${member.name}: $message';
}
38 changes: 14 additions & 24 deletions pkg/vm/lib/transformations/vm_constant_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import 'pragma.dart';
/// fields and getters annotated with "vm:platform-const".
///
/// To avoid restricting getters annotated with "vm:platform-const" to be just
/// a single return statement whose body is evaluated, we treat annotated
/// getters as const functions. If [enableConstFunctions] is false, then
/// only annotated getters are treated this way.
/// a single return statement whose body is evaluated, as well as handling
/// immediately-invoked closures wrapping complex initializing code in field
/// initializers, we enable constant evaluation of functions.
class VMConstantEvaluator extends ConstantEvaluator {
final TargetOS? _targetOS;
final Map<String, Constant> _constantFields = {};
Expand All @@ -43,15 +43,16 @@ class VMConstantEvaluator extends ConstantEvaluator {
this._targetOS,
this._pragmaParser,
{bool enableTripleShift = false,
bool enableConstFunctions = false,
bool enableAsserts = true,
bool errorOnUnevaluatedConstant = false,
EvaluationMode evaluationMode = EvaluationMode.weak})
: _platformClass = typeEnvironment.coreTypes.platformClass,
super(dartLibrarySupport, backend, component, environmentDefines,
typeEnvironment, errorReporter,
enableTripleShift: enableTripleShift,
enableConstFunctions: enableConstFunctions,
// We use evaluation of const functions for getters and immediately
// invoked closures in field initializers.
enableConstFunctions: true,
enableAsserts: enableAsserts,
errorOnUnevaluatedConstant: errorOnUnevaluatedConstant,
evaluationMode: evaluationMode) {
Expand All @@ -68,10 +69,10 @@ class VMConstantEvaluator extends ConstantEvaluator {
Target target, Component component, TargetOS? targetOS, NnbdMode nnbdMode,
{bool evaluateAnnotations = true,
bool enableTripleShift = false,
bool enableConstFunctions = false,
bool enableConstructorTearOff = false,
bool enableAsserts = true,
bool errorOnUnevaluatedConstant = false,
ErrorReporter? errorReporter,
Map<String, String>? environmentDefines,
CoreTypes? coreTypes,
ClassHierarchy? hierarchy}) {
Expand All @@ -90,11 +91,10 @@ class VMConstantEvaluator extends ConstantEvaluator {
component,
environmentDefines,
typeEnvironment,
const SimpleErrorReporter(),
errorReporter ?? const SimpleErrorReporter(),
targetOS,
ConstantPragmaAnnotationParser(coreTypes, target),
enableTripleShift: enableTripleShift,
enableConstFunctions: enableConstFunctions,
enableAsserts: enableAsserts,
errorOnUnevaluatedConstant: errorOnUnevaluatedConstant,
evaluationMode: EvaluationMode.fromNnbdMode(nnbdMode));
Expand Down Expand Up @@ -128,24 +128,14 @@ class VMConstantEvaluator extends ConstantEvaluator {
}
}

final initializer = target.initializer;
if (initializer == null) {
throw 'Cannot const evaluate annotated field with no initializer';
}
// The base class only evaluates const fields, so the VM constant
// evaluator must manually request the initializer be evaluated.
// instead of just calling super.visitStaticGet(target).
return withNewEnvironment(
() => evaluateExpressionInContext(target, initializer));
}

if (target is Procedure && target.kind == ProcedureKind.Getter) {
// Temporarily enable const functions and use the base class to evaluate
// the getter with appropriate caching/recursive evaluation checks.
final oldEnableConstFunctions = enableConstFunctions;
enableConstFunctions = true;
final result = super.visitStaticGet(node);
enableConstFunctions = oldEnableConstFunctions;
return result;
() => evaluateExpressionInContext(target, target.initializer!));
}

throw 'Expected annotated field with initializer or getter, got $target';
// The base class already handles constant evaluation of a getter.
return super.visitStaticGet(node);
}
}
Loading

0 comments on commit 0eac43b

Please sign in to comment.