blob: 2b7f084556c1f8abdeed6685101e5980a0c8d036 [file]
// Copyright (c) 2026, 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:async';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart';
import 'clients.dart';
import 'dart_runtime_service.dart';
import 'rpc_exceptions.dart';
import 'utils.dart';
/// A helper class which handles `evaluate` and `evaluateInFrame` calls by
/// potentially forwarding compilation requests to an external compilation
/// service like Flutter Tools.
abstract base class ExpressionEvaluator {
ExpressionEvaluator({required this.clients});
@protected
final logger = Logger('$ExpressionEvaluator');
/// The set of [Client]s connected to the service.
final UnmodifiableClientNamedLookup clients;
// External RPC name for compilation service.
static const kExternalCompileExpressionRpc = 'compileExpression';
// Common parameters.
static const kIsolateId = 'isolateId';
static const kExpression = 'expression';
static const kScope = 'scope';
static const kDisableBreakpoints = 'disableBreakpoints';
// ID zone support.
static const kIdZoneId = 'idZoneId';
// `evaluate` specific parameters.
static const kTargetId = 'targetId';
// `evaluateInFrame` specific parameters.
static const kFrameIndex = 'frameIndex';
// Keys for compile expression RPC.
static const kKernelBytes = 'kernelBytes';
static const kDefinitions = 'definitions';
static const kDefinitionTypes = 'definitionTypes';
static const kTypeDefinitions = 'typeDefinitions';
static const kTypeBounds = 'typeBounds';
static const kTypeDefaults = 'typeDefaults';
static const kLibraryUri = 'libraryUri';
static const kTokenPos = 'tokenPos';
static const kIsStatic = 'isStatic';
static const kKlass = 'klass';
static const kMethod = 'method';
static const kScriptUri = 'scriptUri';
static const kRootLibraryUri = 'rootLibraryUri';
// Keys for scope response.
static const kParamNames = 'param_names';
static const kParamTypes = 'param_types';
static const kTypeParamsNames = 'type_params_names';
static const kTypeParamsBounds = 'type_params_bounds';
static const kTypeParamsDefaults = 'type_params_defaults';
/// The [evaluate] RPC is used to evaluate an expression in the context of
/// some target.
///
/// `targetId` may refer to a [Library], [Class], or [Instance].
///
/// If `targetId` is a temporary ID which has expired, then an expired
/// [Sentinel] is returned.
///
/// If `targetId` refers to an object which has been collected, then a
/// collected [Sentinel] is returned.
///
/// If `isolateId` refers to an isolate which has exited, then a collected
/// [Sentinel] is returned.
///
/// If `scope` is provided, it should be a map from identifiers to object
/// IDs. These bindings will be added to the scope in which the expression
/// is evaluated, which is a child scope of the class or library for
/// instance/class or library targets respectively. This means bindings
/// provided in scope may shadow instance members, class members and top-level
/// members.
///
/// If `disableBreakpoints` is provided and set to true, any breakpoints hit
/// as a result of this evaluation are ignored. Defaults to false if not
/// provided.
///
/// If `idZoneId` is provided, temporary IDs for [InstanceRef]s and
/// [Instance]s in the RPC response will be allocated in the specified ID
/// zone. If `idZoneId` is omitted, ID allocations will be performed in the
/// default ID zone for the isolate.
///
/// If the expression fails to parse and compile, then
/// [RpcException.expressionCompilationError] will be thrown.
///
/// If an error occurs while evaluating the expression, an [ErrorRef] will be
/// returned.
///
/// If the expression is evaluated successfully, an [InstanceRef] will be
/// returned.
Future<RpcResponse> evaluate(json_rpc.Parameters parameters) {
return _execute(
disableBreakpoints: parameters[kDisableBreakpoints].exists
? parameters[kDisableBreakpoints].asBool
: null,
expression: parameters[kExpression].asString,
idZoneId: parameters[kIdZoneId].exists
? parameters[kIdZoneId].asString
: null,
isolateId: parameters[kIsolateId].asString,
method: 'evaluate',
scope: parameters[kScope].exists
? parameters[kScope].asMap.cast<String, String>()
: null,
targetId: parameters[kTargetId].asString,
);
}
/// The [evaluateInFrame] RPC is used to evaluate an expression in the context
/// of a particular stack frame.
///
/// `frameIndex` is the index of the desired [Frame], with an index of 0
/// indicating the top (most recent) frame.
///
/// If `isolateId` refers to an isolate which has exited, then a collected
/// [Sentinel] is returned.
///
/// If `scope` is provided, it should be a map from identifiers to object
/// IDs. These bindings will be added to the scope in which the expression
/// is evaluated, which is a child scope of the class or library for
/// instance/class or library targets respectively. This means bindings
/// provided in scope may shadow instance members, class members and top-level
/// members.
///
/// If `disableBreakpoints` is provided and set to true, any breakpoints hit
/// as a result of this evaluation are ignored. Defaults to false if not
/// provided.
///
/// If `idZoneId` is provided, temporary IDs for [InstanceRef]s and
/// [Instance]s in the RPC response will be allocated in the specified ID
/// zone. If `idZoneId` is omitted, ID allocations will be performed in the
/// default ID zone for the isolate.
///
/// If the expression fails to parse and compile, then
/// [RpcException.expressionCompilationError] will be thrown.
///
/// If an error occurs while evaluating the expression, an [ErrorRef] will be
/// returned.
///
/// If the expression is evaluated successfully, an [InstanceRef] will be
/// returned.
Future<RpcResponse> evaluateInFrame(json_rpc.Parameters parameters) {
return _execute(
disableBreakpoints: parameters[kDisableBreakpoints].exists
? parameters[kDisableBreakpoints].asBool
: null,
expression: parameters[kExpression].asString,
frameIndex: parameters[kFrameIndex].asInt,
idZoneId: parameters[kIdZoneId].exists
? parameters[kIdZoneId].asString
: null,
isolateId: parameters[kIsolateId].asString,
method: 'evaluateInFrame',
scope: parameters[kScope].exists
? parameters[kScope].asMap.cast<String, String>()
: null,
);
}
/// The common implementation of `evaluate` and `evaluateInFrame`.
///
/// Parameters for each RPC are used by the VM or target when building the
/// scope to determine whether or not we're executing in the context of a
/// frame. Otherwise, the compilation and evaluation pipeline is identical.
Future<RpcResponse> _execute({
required String expression,
required String isolateId,
required String method,
bool? disableBreakpoints,
int? frameIndex,
String? idZoneId,
Map<String, String>? scope,
String? targetId,
}) async {
final compileClient = clients.findFirstClientThatHandlesService(
kExternalCompileExpressionRpc,
);
if (compileClient != null) {
logger.info(
'Found external $kExternalCompileExpressionRpc service: '
'$compileClient',
);
final scopeResult = await buildScope(
frameIndex: frameIndex,
isolateId: isolateId,
scope: scope,
targetId: targetId,
);
final compileParams = buildCompileParams(
expression: expression,
isolateId: isolateId,
scope: scopeResult,
);
RpcResponse compileResponse;
try {
compileResponse = await compileClient.sendRequest(
method: kExternalCompileExpressionRpc,
parameters: compileParams,
);
} on json_rpc.RpcException catch (e) {
logger.warning('Failed to compile expression: $e (${e.data}).');
RpcException.expressionCompilationError.throwException(data: e.data);
}
final kernelBase64 = switch (compileResponse) {
{kKernelBytes: final String k} => k,
{'result': final String k} => k,
_ => null,
};
if (kernelBase64 == null) {
RpcException.expressionCompilationError.throwException(
data: compileResponse['error'],
);
}
return await evaluateCompiledExpression(
disableBreakpoints: disableBreakpoints,
expression: expression,
frameIndex: frameIndex,
idZoneId: idZoneId,
isolateId: isolateId,
kernelBase64: kernelBase64,
scope: scope,
targetId: targetId,
);
}
return await fallbackCompileAndEvaluate(
disableBreakpoints: disableBreakpoints,
expression: expression,
frameIndex: frameIndex,
idZoneId: idZoneId,
isolateId: isolateId,
method: method,
scope: scope,
targetId: targetId,
);
}
/// Builds the scope information for the evaluation context.
@protected
Future<Map<String, Object?>> buildScope({
required String isolateId,
int? frameIndex,
Map<String, String>? scope,
String? targetId,
});
/// Evaluates a compiled expression given its [kernelBase64] kernel bytecode.
@protected
Future<RpcResponse> evaluateCompiledExpression({
required String expression,
required String isolateId,
required String kernelBase64,
bool? disableBreakpoints,
int? frameIndex,
String? idZoneId,
Map<String, String>? scope,
String? targetId,
});
/// Fallback compilation and evaluation when no external compiler is
/// registered.
@protected
Future<RpcResponse> fallbackCompileAndEvaluate({
required String expression,
required String isolateId,
required String method,
bool? disableBreakpoints,
int? frameIndex,
String? idZoneId,
Map<String, String>? scope,
String? targetId,
});
/// Builds the compilation parameters map from the scope response.
@protected
Map<String, Object?> buildCompileParams({
required String expression,
required String isolateId,
required Map<String, Object?> scope,
}) {
final compileParams = <String, Object?>{
kDefinitions: scope[kParamNames],
kDefinitionTypes: scope[kParamTypes],
kExpression: expression,
kIsolateId: isolateId,
kIsStatic: scope[kIsStatic],
kLibraryUri: scope[kLibraryUri],
kMethod: scope[kMethod],
kTokenPos: scope[kTokenPos],
kTypeBounds: scope[kTypeParamsBounds],
kTypeDefaults: scope[kTypeParamsDefaults],
kTypeDefinitions: scope[kTypeParamsNames],
};
final klass = scope[kKlass];
if (klass != null) {
compileParams[kKlass] = klass;
}
final scriptUri = scope[kScriptUri];
if (scriptUri != null) {
compileParams[kScriptUri] = scriptUri;
}
return compileParams;
}
}